package eu.hgross.blaubot.blaubotcam.server; import com.github.sarxos.webcam.Webcam; import com.github.sarxos.webcam.WebcamEvent; import com.github.sarxos.webcam.WebcamListener; import com.github.sarxos.webcam.WebcamPanel; import com.github.sarxos.webcam.WebcamResolution; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Date; import java.util.Hashtable; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSlider; import javax.swing.JToggleButton; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import eu.hgross.blaubot.blaubotcam.server.model.ImageMessage; import eu.hgross.blaubot.blaubotcam.server.ui.VideoViewerPanel; import eu.hgross.blaubot.core.BlaubotDevice; import eu.hgross.blaubot.core.BlaubotFactory; import eu.hgross.blaubot.core.BlaubotKingdom; import eu.hgross.blaubot.core.BlaubotServer; import eu.hgross.blaubot.core.IBlaubotDevice; import eu.hgross.blaubot.core.IBlaubotServerLifeCycleListener; import eu.hgross.blaubot.core.ILifecycleListener; import eu.hgross.blaubot.messaging.BlaubotChannelConfig; import eu.hgross.blaubot.messaging.IBlaubotChannel; import eu.hgross.blaubot.ui.BlaubotServerPanel; public class BlaubotCamServer { /** * The channel on which image data is sent */ public static final short DEFAULT_VIDEO_CHANNEL_ID = 1; public static final int DEFAULT_BLAUBOT_WEBSOCKET_PORT = 8080; public static final String DEFAULT_BLAUBOT_SERVER_UNIQUE_DEVICE_ID = "BlaubotCamServer"; public static final int DEFAULT_JPEG_QUALITY = 20; /** * The port over which the received image data is served as mjpeg stream. */ public static final int DEFAULT_HTTP_PORT_IP_CAM_SERVER = 8081; /* CLI OPTIONS FROM HERE */ private static final String CLI_OPTION_DONT_SERVE = "dontServe"; private static final String CLI_OPTION_NO_SERVER_VIEW = "noServerView"; private static final String CLI_OPTION_NO_CAM_UI = "noCamUi"; private static final String CLI_OPTION_NO_UI = "noUi"; private static final String CLI_OPTION_SERVER_UNIQUE_DEVICE_ID = "serverUniqueDeviceId"; private static final String CLI_OPTION_WEBSOCKET_PORT = "websocketPort"; private static final String CLI_OPTION_VIDEO_CHANNEL_ID = "videoChannelId"; private static final String CLI_OPTION_IP_CAM_HTTP_PORT = "ipCamHttpPort"; private static final String CLI_OPTION_LIST_WEBCAMS = "listCams"; private static final String CLI_OPTION_USE_WEBCAM = "useCam"; private static final String CLI_OPTION_DONT_USE_WEBCAM = "noCam"; private static final String CLI_OPTION_NO_WEBCAM_PREVIEW= "noWebcamPreview"; public static void main(String[] args) throws ClassNotFoundException { // Command line parsing ... Options options = new Options(); options.addOption("n", CLI_OPTION_NO_UI, false, "Do not use any user interface."); options.addOption("ncui", CLI_OPTION_NO_CAM_UI, false, "Do not use the cam ui (no ui to display received images)"); options.addOption("nsui", CLI_OPTION_NO_SERVER_VIEW, false, "Do not use the debug ui that visualizes the blaubot server state."); options.addOption("ds", CLI_OPTION_DONT_SERVE, false, "Do not start the http interface."); options.addOption("nlcui", CLI_OPTION_NO_WEBCAM_PREVIEW, false, "Do not show a preview ui for the locally used webcam (if any)."); options.addOption(Option.builder().longOpt(CLI_OPTION_SERVER_UNIQUE_DEVICE_ID).desc("The server's unique device id (default: " + DEFAULT_BLAUBOT_SERVER_UNIQUE_DEVICE_ID + ").").hasArg().argName("uniqueDeviceId").type(String.class).build()); options.addOption(Option.builder().longOpt(CLI_OPTION_WEBSOCKET_PORT).desc("The port to be used by the websocket acceptor (default: " + DEFAULT_BLAUBOT_WEBSOCKET_PORT + ").").hasArg().argName("webSocketPort").type(Number.class).build()); options.addOption(Option.builder().longOpt(CLI_OPTION_VIDEO_CHANNEL_ID).desc("The channel id used to receive ImageMessages (default: " + DEFAULT_VIDEO_CHANNEL_ID + ").").hasArg().argName("channelId").type(Number.class).build()); options.addOption(Option.builder().longOpt(CLI_OPTION_IP_CAM_HTTP_PORT).desc("The http port used to serve images received from the video channel (default: " + DEFAULT_HTTP_PORT_IP_CAM_SERVER + ").").hasArg().argName("httpPort").type(Number.class).build()); options.addOption("l", CLI_OPTION_LIST_WEBCAMS, false, "Lists available cams and their ids connected to THIS host."); options.addOption("nc", CLI_OPTION_DONT_USE_WEBCAM, false, "If set, no cam will be opened. Overrides " + CLI_OPTION_USE_WEBCAM); options.addOption(Option.builder().longOpt(CLI_OPTION_USE_WEBCAM).desc("Use the specified cam (use --" + CLI_OPTION_LIST_WEBCAMS + " for ids; defaults is the first discovered cam).").hasArg().argName("webcamId").type(Number.class).build()); CommandLineParser parser = new DefaultParser(); try { // parse the command line arguments CommandLine cli = parser.parse(options, args); // fetch the parsed command line arguments boolean listOfWebcamsRequested = cli.hasOption(CLI_OPTION_LIST_WEBCAMS); boolean noUserInterface = cli.hasOption(CLI_OPTION_NO_UI); boolean showDebugView = !noUserInterface && !cli.hasOption(CLI_OPTION_NO_SERVER_VIEW); final boolean showVideoViewer = !noUserInterface && !cli.hasOption(CLI_OPTION_NO_CAM_UI); boolean startIpCamServer = !cli.hasOption(CLI_OPTION_DONT_SERVE); boolean showWebcamUi = !noUserInterface && !cli.hasOption(CLI_OPTION_NO_WEBCAM_PREVIEW); boolean useWebCam = !cli.hasOption(CLI_OPTION_DONT_USE_WEBCAM); final int webSocketPort = cli.hasOption(CLI_OPTION_WEBSOCKET_PORT) ? ((Number) cli.getParsedOptionValue(CLI_OPTION_WEBSOCKET_PORT)).intValue() : DEFAULT_BLAUBOT_WEBSOCKET_PORT; final int ipCamServerPort = cli.hasOption(CLI_OPTION_IP_CAM_HTTP_PORT) ? ((Number) cli.getParsedOptionValue(CLI_OPTION_IP_CAM_HTTP_PORT)).intValue() : DEFAULT_HTTP_PORT_IP_CAM_SERVER; final String serverUniqueDeviceId = cli.hasOption(CLI_OPTION_SERVER_UNIQUE_DEVICE_ID) ? (String) cli.getParsedOptionValue(CLI_OPTION_SERVER_UNIQUE_DEVICE_ID) : DEFAULT_BLAUBOT_SERVER_UNIQUE_DEVICE_ID; final short videoChannelId = cli.hasOption(CLI_OPTION_VIDEO_CHANNEL_ID) ? ((Number) cli.getParsedOptionValue(CLI_OPTION_VIDEO_CHANNEL_ID)).shortValue() : DEFAULT_VIDEO_CHANNEL_ID; final int webCamDeviceToUse = cli.hasOption(CLI_OPTION_USE_WEBCAM) ? ((Number)cli.getParsedOptionValue(CLI_OPTION_USE_WEBCAM)).intValue() : -1; // when a list of webcams is requested, check for available cams, print out the list and exit if (listOfWebcamsRequested) { printCamList(); // nothing more to do System.exit(0); } // choose the webcam final List<Webcam> webcams = Webcam.getWebcams(); final Webcam webcam; if (useWebCam && webCamDeviceToUse >= 0) { // search for this webcam if (webCamDeviceToUse < webcams.size()) { webcam = webcams.get(webCamDeviceToUse); System.out.println("Using cam " + webcam.getName() + "."); } else { webcam = null; System.out.println("Error: Cam with number " + webCamDeviceToUse + " does not exist."); System.out.println("Available cams are:"); printCamList(); System.exit(1); } } else if (useWebCam) { webcam = Webcam.getDefault(); } else { webcam = null; } // we set up a imageWriter for jpeg compression final ImageWriter imageWriter = ImageIO.getImageWritersByFormatName("jpeg").next(); final ImageWriteParam imageWriterParam = imageWriter.getDefaultWriteParam(); imageWriterParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); // Needed see javadoc imageWriterParam.setCompressionQuality((float) DEFAULT_JPEG_QUALITY/100f); // Highest quality // global boolean by which it is decided, if we publish images from our own webcam to the video channel final AtomicBoolean publishCamImagesToChannels = new AtomicBoolean(false); // open webcam, if any if (webcam != null) { webcam.setViewSize(WebcamResolution.VGA.getSize()); // open in async mode final boolean opened = webcam.open(true); if (!opened) { // failed to open the webcam System.out.println("ERROR: could not open webcam device: " + webcam.getName()); } // open preview ui, if cam could be opened and was not explicitly deactivated if (showWebcamUi && opened) { new Thread(new Runnable() { @Override public void run() { final WebcamPanel webcamPanel = new WebcamPanel(webcam); webcamPanel.setFPSLimited(true); // webcamPanel.setFPSLimit(1); // webcamPanel.setFPSDisplayed(true); // webcamPanel.setDisplayDebugInfo(true); // webcamPanel.setImageSizeDisplayed(true); // webcamPanel.setMirrored(true); // ToggleButton to enable/disable sending the cam images to the connected devices final String buttonText = "publishing to connected devices"; final JToggleButton toggleSendVideoButton = new JToggleButton("", false); final ActionListener l = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { final boolean selected = toggleSendVideoButton.isSelected(); publishCamImagesToChannels.set(selected); toggleSendVideoButton.setText(selected ? buttonText : "not " + buttonText); } }; // set initial state text and attach l.actionPerformed(null); toggleSendVideoButton.addActionListener(l); // Jpeg quality slider final JSlider jpegQualitySlider = new JSlider(JSlider.HORIZONTAL, 1, 100, (int) (imageWriterParam.getCompressionQuality() * 100f)); jpegQualitySlider.setMajorTickSpacing(20); jpegQualitySlider.setMinorTickSpacing(5); jpegQualitySlider.setPaintTicks(true); jpegQualitySlider.setPaintLabels(true); Hashtable labelTable = new Hashtable(); labelTable.put(new Integer(1), new JLabel("Low quality")); labelTable.put(new Integer(100), new JLabel("High quality")); jpegQualitySlider.setLabelTable(labelTable); jpegQualitySlider.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { final int quality = jpegQualitySlider.getValue(); final float qualityF = (float) quality / 100f; imageWriterParam.setCompressionQuality(qualityF); } }); // button to enable/disable webcam JButton toggleWebCam = new JButton("Toggle camera"); toggleWebCam.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (webcamPanel.isStarted()) { webcamPanel.stop(); } else { webcamPanel.start(); } } }); // put it all together in a JFrame JPanel mainPanel = new JPanel(); mainPanel.setLayout(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; mainPanel.add(jpegQualitySlider, gbc); gbc.gridy += 1; JPanel buttonPanel = new JPanel(); buttonPanel.add(toggleWebCam); buttonPanel.add(toggleSendVideoButton); mainPanel.add(buttonPanel, gbc); gbc.gridy += 1; mainPanel.add(webcamPanel, gbc); gbc.gridy += 1; JFrame window = new JFrame("Cam " + webcam.getName() + " preview"); window.add(mainPanel); window.setResizable(true); window.pack(); window.setVisible(true); window.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { // TODO we are currently not able to bring it back ... } }); } }).start(); } } // create the server and service final BlaubotServer websocketServer = BlaubotFactory.createBlaubotWebsocketServer(new BlaubotDevice(serverUniqueDeviceId), webSocketPort); final eu.hgross.blaubot.blaubotcam.server.service.IPCamServer ipCamServer = new eu.hgross.blaubot.blaubotcam.server.service.IPCamServer(ipCamServerPort); // start, if not explicitly prevented if (startIpCamServer) { ipCamServer.startHTTPServer(); System.out.println("IpCamServer started. Access it via http://localhost:" + DEFAULT_HTTP_PORT_IP_CAM_SERVER); } // open debug view, if not explicitly prevented if (showDebugView) { BlaubotServerPanel.createAndshowGui(websocketServer); } /** * A mapping to remember the windows we opened for each kingdom. */ final ConcurrentHashMap<BlaubotKingdom, JFrame> frames = new ConcurrentHashMap<>(); websocketServer.addServerLifeCycleListener(new IBlaubotServerLifeCycleListener() { @Override public void onKingdomConnected(final BlaubotKingdom kingdom) { final IBlaubotChannel videoChannel = kingdom.getChannelManager().createOrGetChannel(videoChannelId); videoChannel.getChannelConfig().setMessagePickerStrategy(BlaubotChannelConfig.MessagePickerStrategy.DISCARD_OLD); videoChannel.getChannelConfig().setMessageRateLimit(200); videoChannel.getChannelConfig().setQueueCapacity(10); videoChannel.addMessageListener(ipCamServer); // if a webcam is available, add a listener to publish their data to the channel if (webcam != null && webcam.isOpen()) { webcam.addWebcamListener(new WebcamListener() { @Override public void webcamOpen(WebcamEvent we) { } @Override public void webcamClosed(WebcamEvent we) { } @Override public void webcamDisposed(WebcamEvent we) { } @Override public void webcamImageObtained(WebcamEvent we) { // do nothing, if not explicitly activated if (!publishCamImagesToChannels.get()) { return; } // convert to jpeg // TODO this is messy, because we convert the jpg for each connected kingdom! Put some caching between listener calls and publish final BufferedImage image = we.getImage(); try { final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); final ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(byteArrayOutputStream); synchronized (imageWriter) { imageWriter.setOutput(imageOutputStream); try { imageWriter.write(null, new IIOImage(image, null, null), imageWriterParam); } finally { imageOutputStream.flush(); } } final byte[] bytes = byteArrayOutputStream.toByteArray(); // final byte[] bytes = ImageUtils.toByteArray(image, ImageUtils.FORMAT_JPG); ImageMessage imageMessage = new ImageMessage(serverUniqueDeviceId, bytes, new Date()); videoChannel.publish(imageMessage.toBytes()); } catch (IOException e) { e.printStackTrace(); } } }); } // if the swing ui is not needed, we are finished here if (!showVideoViewer) { return; } // start the swing ui displaying incoming images final VideoViewerPanel videoViewerPanel = new eu.hgross.blaubot.blaubotcam.server.ui.VideoViewerPanel(); kingdom.getChannelManager().addAdminMessageListener(videoViewerPanel); videoChannel.addMessageListener(videoViewerPanel); videoChannel.subscribe(); // show the panel in a frame final JFrame frame = new JFrame(); frames.put(kingdom, frame); frame.setMinimumSize(new Dimension(1024, 500)); frame.add(videoViewerPanel); frame.pack(); frame.setVisible(true); frame.setTitle("Blaubot cam server for kingdom of " + kingdom.getKingDevice().getUniqueDeviceID()); frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { kingdom.getChannelManager().removeAdminMessageListener(videoViewerPanel); videoChannel.removeMessageListener(videoViewerPanel); frames.remove(kingdom); } }); } @Override public void onKingdomDisconnected(BlaubotKingdom kingdom) { // unregister the IpCamServer IBlaubotChannel videoChannel = kingdom.getChannelManager().createOrGetChannel(videoChannelId); videoChannel.removeMessageListener(ipCamServer); // close the frame final JFrame frame = frames.get(kingdom); if (frame == null) { // already closed (by user or something else) or never opened return; } // everything guit related is unregistered properly on close ... so reuse this logic. frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING)); } }); // start blaubot up websocketServer.startBlaubotServer(); } catch (ParseException exp) { // oops, something went wrong System.err.println("Parsing failed. Reason: " + exp.getMessage()); HelpFormatter formatter = new HelpFormatter(); formatter.printHelp(BlaubotCamServer.class.getSimpleName(), options); System.exit(1); } } /** * Prints a list of available web cams to the console. */ private static void printCamList() { final List<Webcam> webcams = Webcam.getWebcams(); System.out.println("Found " + webcams.size() + " available cam" + (webcams.size() > 1 ? "s": "") + " on this machine."); if (webcams.size() > 0) { System.out.println("You can explicitly select one of these cams via the --" + CLI_OPTION_USE_WEBCAM + " <webCamId> command."); System.out.println("ID # Name"); System.out.println("------------------------"); } int id = 0; for (Webcam cam : webcams) { final String name = cam.getDevice().getName(); System.out.println(String.format("%4d # %s", id++, name)); } } }