package com.igormaznitsa.zxpoly.streamer; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.logging.Logger; import org.apache.commons.io.IOUtils; public class InternalHttpServer { private static final Logger LOGGER = Logger.getLogger("VideoStreamer.InternalHttpServer"); private static final String STREAM_RESOURCE = "stream.ts"; private final String mime; private final InetAddress tcpReaderAddress; private final InetAddress httpServerAddress; private final int tcpReaderPort; private final int httpServerPort; private final AtomicReference<HttpServer> httpServerRef = new AtomicReference<>(); private final AtomicReference<TcpReader> tcpReaderRef = new AtomicReference<>(); private final Consumer<InternalHttpServer> stopConsumer; private static final String HTML_TEMPLATE; private volatile boolean stopped; static { try { HTML_TEMPLATE = IOUtils.resourceToString("/com/igormaznitsa/zxpoly/streamer/streamer.html", StandardCharsets.UTF_8); } catch (IOException ex) { throw new Error("Can't read template", ex); } } public InternalHttpServer( final String mime, final InetAddress addressIn, final int portIn, final InetAddress addressOut, final int portOut, final Consumer<InternalHttpServer> stopConsumer ) { this.mime = mime; this.tcpReaderAddress = addressIn; this.httpServerAddress = addressOut; this.tcpReaderPort = portIn; this.httpServerPort = portOut; this.stopConsumer = stopConsumer; } public void start() throws IOException { startTcpServer(); startHttpServer(); } private void startTcpServer() { final TcpReader newReader = new TcpReader("tcp-reader", 0x10000, 10, InetAddress.getLoopbackAddress(), 0); if (this.tcpReaderRef.compareAndSet(null, newReader)) { newReader.addListener(new AbstractTcpSingleThreadServer.TcpServerListener() { @Override public void onConnectionDone(final AbstractTcpSingleThreadServer source, final Socket socket) { if (source == newReader) { LOGGER.info("TCP reader connection lost"); stop(); } } }); newReader.start(); } } private void startHttpServer() throws IOException { final HttpServer server = HttpServer.create(new InetSocketAddress(this.httpServerAddress, this.httpServerPort), 3); if (this.httpServerRef.compareAndSet(null, server)) { server.createContext("/" + STREAM_RESOURCE, this::handleStreamData); server.createContext("/", this::handleMainPage); server.setExecutor(Executors.newSingleThreadExecutor()); server.start(); } } public String getHttpAddress() { final HttpServer server = this.httpServerRef.get(); final InetSocketAddress address = server == null ? null : server.getAddress(); return address == null ? "none" : address.getAddress().getHostAddress() + ":" + address.getPort(); } private void handleMainPage(final HttpExchange exchange) throws IOException { final String linkToVideoStream = "http://" + this.getHttpAddress() + "/" + STREAM_RESOURCE; final String page = HTML_TEMPLATE.replace("${video.link}", linkToVideoStream) .replace("${video.mime}", this.mime); final Headers headers = exchange.getResponseHeaders(); headers.add("Content-Type", "text/html"); final byte[] content = page.getBytes(StandardCharsets.UTF_8); exchange.sendResponseHeaders(200, content.length); final OutputStream out = exchange.getResponseBody(); out.write(content); out.flush(); out.close(); } private void handleStreamData(final HttpExchange exchange) throws IOException { final Headers headers = exchange.getResponseHeaders(); headers.add("Content-Type", this.mime); headers.add("Cache-Control", "no-cache, no-store"); headers.add("Pragma", "no-cache"); headers.add("Transfer-Encoding", "chunked"); headers.add("Content-Transfer-Encoding", "binary"); headers.add("Expires", "0"); headers.add("Connection", "Keep-Alive"); headers.add("Keep-Alive", "max"); headers.add("Accept-Ranges", "none"); exchange.sendResponseHeaders(200, 0); OutputStream os = exchange.getResponseBody(); try { final TcpReader reader = this.tcpReaderRef.get(); if (reader != null) { while (!stopped && !Thread.currentThread().isInterrupted()) { final byte[] data = reader.read(); if (data != null) { os.write(data); os.flush(); } } } } finally { os.close(); } } public void stop() { final HttpServer server = this.httpServerRef.getAndSet(null); if (server != null) { final TcpReader reader = this.tcpReaderRef.getAndSet(null); if (reader != null) { reader.stop(); } try { server.stop(0); } catch (Exception ex) { LOGGER.warning("Error on server stop: " + ex.getMessage()); } if (this.stopConsumer != null) { this.stopConsumer.accept(this); } } } public String getTcpAddress() { final TcpReader reader = this.tcpReaderRef.get(); return reader == null ? "none" : reader.getServerAddress(); } }