@Override public Response serve(String uri, String method, Properties header, Properties parms) { if (!uri.equals("/mediaserver")) { return super.serve(uri, method, header, parms); // this way we can // also serve up // normal files and // content } logger.fine(method + " '" + uri + "' "); Enumeration<?> e = header.propertyNames(); while (e.hasMoreElements()) { String value = (String) e.nextElement(); logger.fine(" HDR: '" + value + "' = '" + header.getProperty(value) + "'"); } e = parms.propertyNames(); while (e.hasMoreElements()) { String value = (String) e.nextElement(); logger.fine(" PRM: '" + value + "' = '" + parms.getProperty(value) + "'"); } // TODO: check the actual path... final String mediaPath = parms.getProperty("media"); final String outputFormatStr = parms.getProperty("format"); final String mimeType = parms.getProperty("mime"); logger.info("requested media: " + mediaPath); logger.info("requested mime type: " + mimeType); if (mediaPath == null) return new Response(HTTP_FORBIDDEN, "text/plain", "mediaPath parameter not specified"); if (mimeType == null) return new Response(HTTP_FORBIDDEN, "text/plain", "mimeType parameter not specified"); // TODO: if we aren't performing any transcoding, just serve the file up // directly. // TODO: capture sources need to be treated as singletons, with some // kind of broadcasting/cloning to ensure // that multiple connections can be made. final String serverSideUrlStr = mediaPath; // URLUtils.createUrlStr(new // File(mediaPath)); // TODO: // enforce that we can't just // serve up anything anywhere final ContentDescriptor outputContentDescriptor = new FileTypeDescriptor(ContentDescriptor.mimeTypeToPackageName(mimeType)); final Format outputFormat; if (outputFormatStr == null) { outputFormat = null; } else { try { outputFormat = FormatArgUtils.parse(outputFormatStr); } catch (ParseException e1) { logger.log(Level.WARNING, "" + e1, e1); return new Response(HTTP_FORBIDDEN, "text/plain", "" + e1); } } logger.info("serverSideUrlStr: " + serverSideUrlStr); logger.info("outputContentDescriptor: " + outputContentDescriptor); logger.info("outputFormat: " + outputFormat); final InputStream is; try { is = getInputStream(serverSideUrlStr, outputFormat, outputContentDescriptor); } catch (Exception e1) { return new Response(HTTP_FORBIDDEN, "text/plain", "" + e1); } final String responseMimeType; // workaround for the problem that the multipart/x-mixed-replace // boundary is not stored anywhere. // this assumes that if we are serving multipart/x-mixed-replace data, // that MultipartMixedReplaceMux is being used. if (mimeType.equals("multipart/x-mixed-replace")) responseMimeType = mimeType + ";boundary=" + MultipartMixedReplaceMux.BOUNDARY; else responseMimeType = mimeType; logger.info("Response mime type: " + responseMimeType); return new Response(HTTP_OK, responseMimeType, is); }
/** * Implements {@link ControllerListener#controllerUpdate(ControllerEvent)}. Handles events from * the <tt>Processor</tt>s that this instance uses to transcode media. * * @param ev the event to handle. */ public void controllerUpdate(ControllerEvent ev) { if (ev == null || ev.getSourceController() == null) { return; } Processor processor = (Processor) ev.getSourceController(); ReceiveStreamDesc desc = findReceiveStream(processor); if (desc == null) { logger.warn("Event from an orphaned processor, ignoring: " + ev); return; } if (ev instanceof ConfigureCompleteEvent) { if (logger.isInfoEnabled()) { logger.info( "Configured processor for ReceiveStream ssrc=" + desc.ssrc + " (" + desc.format + ")" + " " + System.currentTimeMillis()); } boolean audio = desc.format instanceof AudioFormat; if (audio) { ContentDescriptor cd = processor.setContentDescriptor(AUDIO_CONTENT_DESCRIPTOR); if (!AUDIO_CONTENT_DESCRIPTOR.equals(cd)) { logger.error( "Failed to set the Processor content " + "descriptor to " + AUDIO_CONTENT_DESCRIPTOR + ". Actual result: " + cd); removeReceiveStream(desc, false); return; } } for (TrackControl track : processor.getTrackControls()) { Format trackFormat = track.getFormat(); if (audio) { final long ssrc = desc.ssrc; SilenceEffect silenceEffect; if (Constants.OPUS_RTP.equals(desc.format.getEncoding())) { silenceEffect = new SilenceEffect(48000); } else { // We haven't tested that the RTP timestamps survive // the journey through the chain when codecs other than // opus are in use, so for the moment we rely on FMJ's // timestamps for non-opus formats. silenceEffect = new SilenceEffect(); } silenceEffect.setListener( new SilenceEffect.Listener() { boolean first = true; @Override public void onSilenceNotInserted(long timestamp) { if (first) { first = false; // send event only audioRecordingStarted(ssrc, timestamp); } else { // change file and send event resetRecording(ssrc, timestamp); } } }); desc.silenceEffect = silenceEffect; AudioLevelEffect audioLevelEffect = new AudioLevelEffect(); audioLevelEffect.setAudioLevelListener( new SimpleAudioLevelListener() { @Override public void audioLevelChanged(int level) { activeSpeakerDetector.levelChanged(ssrc, level); } }); try { // We add an effect, which will insert "silence" in // place of lost packets. track.setCodecChain(new Codec[] {silenceEffect, audioLevelEffect}); } catch (UnsupportedPlugInException upie) { logger.warn("Failed to insert silence effect: " + upie); // But do go on, a recording without extra silence is // better than nothing ;) } } else { // transcode vp8/rtp to vp8 (i.e. depacketize vp8) if (trackFormat.matches(vp8RtpFormat)) track.setFormat(vp8Format); else { logger.error("Unsupported track format: " + trackFormat + " for ssrc=" + desc.ssrc); // we currently only support vp8 removeReceiveStream(desc, false); return; } } } processor.realize(); } else if (ev instanceof RealizeCompleteEvent) { desc.dataSource = processor.getDataOutput(); long ssrc = desc.ssrc; boolean audio = desc.format instanceof AudioFormat; String suffix = audio ? AUDIO_FILENAME_SUFFIX : VIDEO_FILENAME_SUFFIX; // XXX '\' on windows? String filename = getNextFilename(path + "/" + ssrc, suffix); desc.filename = filename; DataSink dataSink; if (audio) { try { dataSink = Manager.createDataSink(desc.dataSource, new MediaLocator("file:" + filename)); } catch (NoDataSinkException ndse) { logger.error("Could not create DataSink: " + ndse); removeReceiveStream(desc, false); return; } } else { dataSink = new WebmDataSink(filename, desc.dataSource); } if (logger.isInfoEnabled()) logger.info( "Created DataSink (" + dataSink + ") for SSRC=" + ssrc + ". Output filename: " + filename); try { dataSink.open(); } catch (IOException e) { logger.error("Failed to open DataSink (" + dataSink + ") for" + " SSRC=" + ssrc + ": " + e); removeReceiveStream(desc, false); return; } if (!audio) { final WebmDataSink webmDataSink = (WebmDataSink) dataSink; webmDataSink.setSsrc(ssrc); webmDataSink.setEventHandler(eventHandler); webmDataSink.setKeyFrameControl( new KeyFrameControlAdapter() { @Override public boolean requestKeyFrame(boolean urgent) { return requestFIR(webmDataSink); } }); } try { dataSink.start(); } catch (IOException e) { logger.error( "Failed to start DataSink (" + dataSink + ") for" + " SSRC=" + ssrc + ". " + e); removeReceiveStream(desc, false); return; } if (logger.isInfoEnabled()) logger.info("Started DataSink for SSRC=" + ssrc); desc.dataSink = dataSink; processor.start(); } else if (logger.isDebugEnabled()) { logger.debug( "Unhandled ControllerEvent from the Processor for ssrc=" + desc.ssrc + ": " + ev); } }