Beispiel #1
1
  /**
   * {@inheritDoc}
   *
   * @param format unused, since this implementation records multiple streams using potentially
   *     different formats.
   * @param dirname the path to the directory into which this <tt>Recorder</tt> will store the
   *     recorded media files.
   */
  @Override
  public void start(String format, String dirname) throws IOException, MediaException {
    if (logger.isInfoEnabled()) logger.info("Starting, format=" + format + " " + hashCode());
    path = dirname;

    MediaService mediaService = LibJitsi.getMediaService();

    /*
     * Note that we use only one RTPConnector for both the RTPTranslator
     * and the RTPManager instances. The this.translator will write to its
     * output streams, and this.rtpManager will read from its input streams.
     */
    rtpConnector = new RTPConnectorImpl(redPayloadType, ulpfecPayloadType);

    rtpManager = RTPManager.newInstance();

    /*
     * Add the formats that we know about.
     */
    rtpManager.addFormat(vp8RtpFormat, vp8PayloadType);
    rtpManager.addFormat(opusFormat, opusPayloadType);
    rtpManager.addReceiveStreamListener(this);

    /*
     * Note: When this.rtpManager sends RTCP sender/receiver reports, they
     * will end up being written to its own input stream. This is not
     * expected to cause problems, but might be something to keep an eye on.
     */
    rtpManager.initialize(rtpConnector);

    /*
     * Register a fake call participant.
     * TODO: can we use a more generic MediaStream here?
     */
    streamRTPManager =
        new StreamRTPManager(
            mediaService.createMediaStream(
                new MediaDeviceImpl(new CaptureDeviceInfo(), MediaType.VIDEO)),
            translator);

    streamRTPManager.initialize(rtpConnector);

    rtcpFeedbackSender = translator.getRtcpFeedbackMessageSender();

    translator.addFormat(streamRTPManager, opusFormat, opusPayloadType);

    // ((RTPTranslatorImpl)videoRTPTranslator).addFormat(streamRTPManager, redFormat,
    // redPayloadType);
    // ((RTPTranslatorImpl)videoRTPTranslator).addFormat(streamRTPManager, ulpfecFormat,
    // ulpfecPayloadType);
    // ((RTPTranslatorImpl)videoRTPTranslator).addFormat(streamRTPManager,
    // mediaFormatImpl.getFormat(), vp8PayloadType);

    started = true;
  }
Beispiel #2
0
  /**
   * Logs the specified message and details.
   *
   * @param message the message to log
   * @param from the message sender
   * @param to the message addressee
   * @param status message status
   * @param sender determines whether we are the origin of this message.
   */
  public void logMessage(
      SIPMessage message, String from, String to, String status, boolean sender) {
    if (!logger.isInfoEnabled()) return;

    String msgHeader;

    if (sender) msgHeader = "JAIN-SIP sent a message from=\"";
    else msgHeader = "JAIN-SIP received a message from=\"";

    if (logger.isInfoEnabled())
      logger.info(msgHeader + from + "\" to=\"" + to + "\" (status: " + status + "):\n" + message);
  }
  /**
   * Gets the <tt>MediaFormat</tt>s among the specified <tt>mediaFormats</tt> which have the
   * specified <tt>encoding</tt> and, optionally, <tt>clockRate</tt>.
   *
   * @param mediaFormats the <tt>MediaFormat</tt>s from which to filter out only the ones which have
   *     the specified <tt>encoding</tt> and, optionally, <tt>clockRate</tt>
   * @param encoding the well-known encoding (name) of the <tt>MediaFormat</tt>s to be retrieved
   * @param clockRate the clock rate of the <tt>MediaFormat</tt>s to be retrieved; {@link
   *     #CLOCK_RATE_NOT_SPECIFIED} if any clock rate is acceptable
   * @return a <tt>List</tt> of the <tt>MediaFormat</tt>s among <tt>mediaFormats</tt> which have the
   *     specified <tt>encoding</tt> and, optionally, <tt>clockRate</tt>
   */
  private List<MediaFormat> getMatchingMediaFormats(
      MediaFormat[] mediaFormats, String encoding, double clockRate) {
    /*
     * XXX Use String#equalsIgnoreCase(String) because some clients transmit
     * some of the codecs starting with capital letters.
     */

    /*
     * As per RFC 3551.4.5.2, because of a mistake in RFC 1890 and for
     * backward compatibility, G.722 should always be announced as 8000 even
     * though it is wideband. So, if someone is looking for G722/16000,
     * then: Forgive them, for they know not what they do!
     */
    if ("G722".equalsIgnoreCase(encoding) && (16000 == clockRate)) {
      clockRate = 8000;
      if (logger.isInfoEnabled()) logger.info("Suppressing erroneous 16000 announcement for G.722");
    }

    List<MediaFormat> supportedMediaFormats = new ArrayList<MediaFormat>();

    for (MediaFormat mediaFormat : mediaFormats) {
      if (mediaFormat.getEncoding().equalsIgnoreCase(encoding)
          && ((CLOCK_RATE_NOT_SPECIFIED == clockRate)
              || (mediaFormat.getClockRate() == clockRate))) {
        supportedMediaFormats.add(mediaFormat);
      }
    }
    return supportedMediaFormats;
  }
Beispiel #4
0
  /**
   * Restarts the recording for a specific SSRC.
   *
   * @param ssrc the SSRC for which to restart recording. RTP packet of the new recording).
   */
  private void resetRecording(long ssrc, long timestamp) {
    ReceiveStreamDesc receiveStream = findReceiveStream(ssrc);

    // we only restart audio recordings
    if (receiveStream != null && receiveStream.format instanceof AudioFormat) {
      String newFilename = getNextFilename(path + "/" + ssrc, AUDIO_FILENAME_SUFFIX);

      // flush the buffer contained in the MP3 encoder
      String s = "trying to flush ssrc=" + ssrc;
      Processor p = receiveStream.processor;
      if (p != null) {
        s += " p!=null";
        for (TrackControl tc : p.getTrackControls()) {
          Object o = tc.getControl(FlushableControl.class.getName());
          if (o != null) ((FlushableControl) o).flush();
        }
      }

      if (logger.isInfoEnabled()) {
        logger.info("Restarting recording for SSRC=" + ssrc + ". New filename: " + newFilename);
      }

      receiveStream.dataSink.close();
      receiveStream.dataSink = null;

      // flush the FMJ jitter buffer
      // DataSource ds = receiveStream.receiveStream.getDataSource();
      // if (ds instanceof net.sf.fmj.media.protocol.rtp.DataSource)
      //    ((net.sf.fmj.media.protocol.rtp.DataSource)ds).flush();

      receiveStream.filename = newFilename;
      try {
        receiveStream.dataSink =
            Manager.createDataSink(
                receiveStream.dataSource, new MediaLocator("file:" + newFilename));
      } catch (NoDataSinkException ndse) {
        logger.warn("Could not reset recording for SSRC=" + ssrc + ": " + ndse);
        removeReceiveStream(receiveStream, false);
      }

      try {
        receiveStream.dataSink.open();
        receiveStream.dataSink.start();
      } catch (IOException ioe) {
        logger.warn("Could not reset recording for SSRC=" + ssrc + ": " + ioe);
        removeReceiveStream(receiveStream, false);
      }

      audioRecordingStarted(ssrc, timestamp);
    }
  }
Beispiel #5
0
  /**
   * Receives options requests and replies with an OK response containing methods that we support.
   *
   * @param requestEvent the incoming options request.
   * @return <tt>true</tt> if request has been successfully processed, <tt>false</tt> otherwise
   */
  @Override
  public boolean processRequest(RequestEvent requestEvent) {
    Response optionsOK = null;
    try {
      optionsOK =
          provider.getMessageFactory().createResponse(Response.OK, requestEvent.getRequest());

      // add to the allows header all methods that we support
      for (String method : provider.getSupportedMethods()) {
        // don't support REGISTERs
        if (!method.equals(Request.REGISTER))
          optionsOK.addHeader(provider.getHeaderFactory().createAllowHeader(method));
      }

      Iterable<String> knownEventsList = provider.getKnownEventsList();

      synchronized (knownEventsList) {
        for (String event : knownEventsList)
          optionsOK.addHeader(provider.getHeaderFactory().createAllowEventsHeader(event));
      }
    } catch (ParseException ex) {
      // What else could we do apart from logging?
      logger.warn("Failed to create an incoming OPTIONS request", ex);
      return false;
    }

    try {
      SipStackSharing.getOrCreateServerTransaction(requestEvent).sendResponse(optionsOK);
    } catch (TransactionUnavailableException ex) {
      // this means that we received an OPTIONS request outside the scope
      // of a transaction which could mean that someone is simply sending
      // us b****hit to keep a NAT connection alive, so let's not get too
      // excited.
      if (logger.isInfoEnabled())
        logger.info("Failed to respond to an incoming " + "transactionless OPTIONS request");
      if (logger.isTraceEnabled()) logger.trace("Exception was:", ex);
      return false;
    } catch (InvalidArgumentException ex) {
      // What else could we do apart from logging?
      logger.warn("Failed to send an incoming OPTIONS request", ex);
      return false;
    } catch (SipException ex) {
      // What else could we do apart from logging?
      logger.warn("Failed to send an incoming OPTIONS request", ex);
      return false;
    }

    return true;
  }
  /**
   * Processes the session initiation {@link SessionIQ} that we were created with, passing its
   * content to the media handler.
   *
   * @param sessionInitIQ The {@link SessionIQ} that created the session that we are handling here.
   */
  public void processSessionAccept(SessionIQ sessionInitIQ) {
    this.sessionInitIQ = sessionInitIQ;

    CallPeerMediaHandlerGTalkImpl mediaHandler = getMediaHandler();
    Collection<PacketExtension> extensions = sessionInitIQ.getExtensions();
    RtpDescriptionPacketExtension answer = null;

    for (PacketExtension ext : extensions) {
      if (ext.getElementName().equalsIgnoreCase(RtpDescriptionPacketExtension.ELEMENT_NAME)) {
        answer = (RtpDescriptionPacketExtension) ext;
        break;
      }
    }

    try {
      mediaHandler.getTransportManager().wrapupConnectivityEstablishment();
      mediaHandler.processAnswer(answer);
    } catch (IllegalArgumentException e) {
      // HACK for FreeSwitch that send accept message before sending
      // candidates
      sessAcceptedWithNoCands = sessionInitIQ;
      return;
    } catch (Exception exc) {
      if (logger.isInfoEnabled()) logger.info("Failed to process a session-accept", exc);

      // send an error response
      String reasonText = "Error: " + exc.getMessage();
      SessionIQ errResp =
          GTalkPacketFactory.createSessionTerminate(
              sessionInitIQ.getTo(),
              sessionInitIQ.getFrom(),
              sessionInitIQ.getID(),
              Reason.GENERAL_ERROR,
              reasonText);

      getMediaHandler().getTransportManager().close();
      setState(CallPeerState.FAILED, reasonText);
      getProtocolProvider().getConnection().sendPacket(errResp);
      return;
    }

    // tell everyone we are connecting so that the audio notifications would
    // stop
    setState(CallPeerState.CONNECTED);

    mediaHandler.start();
  }
Beispiel #7
0
  @Override
  public void stop() {
    if (started) {
      if (logger.isInfoEnabled()) logger.info("Stopping " + hashCode());

      // remove the recorder from the translator (e.g. stop new packets from
      // being written to rtpConnector
      if (streamRTPManager != null) streamRTPManager.dispose();

      HashSet<ReceiveStreamDesc> streamsToRemove = new HashSet<ReceiveStreamDesc>();
      synchronized (receiveStreams) {
        streamsToRemove.addAll(receiveStreams);
      }

      for (ReceiveStreamDesc r : streamsToRemove) removeReceiveStream(r, false);

      rtpConnector.rtcpPacketTransformer.close();
      rtpConnector.rtpPacketTransformer.close();
      rtpManager.dispose();

      started = false;
    }
  }
  /**
   * Sets the simulcast layers for this receiver and fires an event about it.
   *
   * @param simulcastLayers the simulcast layers for this receiver.
   */
  public void setSimulcastLayers(SimulcastLayer[] simulcastLayers) {
    this.simulcastLayers = simulcastLayers;

    if (logger.isInfoEnabled()) {
      if (simulcastLayers == null) {
        logInfo("Simulcast disabled.");
      } else {
        for (SimulcastLayer l : simulcastLayers) {
          logInfo(l.getOrder() + ": " + l.getPrimarySSRC());
        }
      }
    }

    executorService.execute(
        new Runnable() {
          public void run() {
            firePropertyChange(SIMULCAST_LAYERS_PNAME, null, null);
          }
        });

    // TODO If simulcastLayers has changed, then simulcastLayerFrameHistory
    // has very likely become irrelevant. In other words, clear
    // simulcastLayerFrameHistory.
  }
Beispiel #9
0
  /**
   * Gathers Jingle Nodes candidates for all host <tt>Candidate</tt>s that are already present in
   * the specified <tt>component</tt>. This method relies on the specified <tt>component</tt> to
   * already contain all its host candidates so that it would resolve them.
   *
   * @param component the {@link Component} that we'd like to gather candidate Jingle Nodes
   *     <tt>Candidate</tt>s for
   * @return the <tt>LocalCandidate</tt>s gathered by this <tt>CandidateHarvester</tt>
   */
  @Override
  public synchronized Collection<LocalCandidate> harvest(Component component) {
    logger.info("harvest Jingle Nodes");

    Collection<LocalCandidate> candidates = new HashSet<LocalCandidate>();
    String ip = null;
    int port = -1;

    /* if we have already a candidate (RTCP) allocated, get it */
    if (localAddressSecond != null && relayedAddressSecond != null) {
      LocalCandidate candidate =
          createJingleNodesCandidate(relayedAddressSecond, component, localAddressSecond);

      // try to add the candidate to the component and then only add it to
      // the harvest not redundant (not sure how it could be red. but ...)
      if (component.addLocalCandidate(candidate)) {
        candidates.add(candidate);
      }

      localAddressSecond = null;
      relayedAddressSecond = null;
      return candidates;
    }

    XMPPConnection conn = serviceNode.getConnection();
    JingleChannelIQ ciq = null;

    if (serviceNode != null) {
      final TrackerEntry preferred = serviceNode.getPreferedRelay();

      if (preferred != null) {
        ciq = SmackServiceNode.getChannel(conn, preferred.getJid());
      }
    }

    if (ciq != null) {
      ip = ciq.getHost();
      port = ciq.getRemoteport();

      if (logger.isInfoEnabled()) {
        logger.info(
            "JN relay: " + ip + " remote port:" + port + " local port: " + ciq.getLocalport());
      }

      if (ip == null || ciq.getRemoteport() == 0) {
        logger.warn("JN relay ignored because ip was null or port 0");
        return candidates;
      }

      // Drop the scope or interface name if the relay sends it
      // along in its IPv6 address. The scope/ifname is only valid on the
      // host that owns the IP and we don't need it here.
      int scopeIndex = ip.indexOf('%');
      if (scopeIndex > 0) {
        logger.warn("Dropping scope from assumed IPv6 address " + ip);
        ip = ip.substring(0, scopeIndex);
      }

      /* RTP */
      TransportAddress relayedAddress = new TransportAddress(ip, port, Transport.UDP);
      TransportAddress localAddress = new TransportAddress(ip, ciq.getLocalport(), Transport.UDP);

      LocalCandidate local = createJingleNodesCandidate(relayedAddress, component, localAddress);

      /* RTCP */
      relayedAddressSecond = new TransportAddress(ip, port + 1, Transport.UDP);
      localAddressSecond = new TransportAddress(ip, ciq.getLocalport() + 1, Transport.UDP);

      // try to add the candidate to the component and then only add it to
      // the harvest not redundant (not sure how it could be red. but ...)
      if (component.addLocalCandidate(local)) {
        candidates.add(local);
      }
    }

    return candidates;
  }
 private void logInfo(String msg) {
   if (logger.isInfoEnabled()) {
     msg = getSimulcastEngine().getVideoChannel().getEndpoint().getID() + ": " + msg;
     logger.info(msg);
   }
 }
  /**
   * Starts the execution of the neomedia bundle in the specified context.
   *
   * @param bundleContext the context in which the neomedia bundle is to start executing
   * @throws Exception if an error occurs while starting the execution of the neomedia bundle in the
   *     specified context
   */
  public void start(BundleContext bundleContext) throws Exception {
    if (logger.isDebugEnabled()) logger.debug("Started.");

    NeomediaActivator.bundleContext = bundleContext;

    // MediaService
    mediaServiceImpl = (MediaServiceImpl) LibJitsi.getMediaService();

    bundleContext.registerService(MediaService.class.getName(), mediaServiceImpl, null);
    if (logger.isDebugEnabled()) logger.debug("Media Service ... [REGISTERED]");

    //        mediaConfiguration = new MediaConfigurationImpl();
    //        bundleContext.registerService(
    //                MediaConfigurationService.class.getStatus(),
    //                getMediaConfiguration(),
    //                null);
    if (logger.isDebugEnabled()) logger.debug("Media Configuration ... [REGISTERED]");

    ConfigurationService cfg = NeomediaActivator.getConfigurationService();
    Dictionary<String, String> mediaProps = new Hashtable<String, String>();

    mediaProps.put(ConfigurationForm.FORM_TYPE, ConfigurationForm.GENERAL_TYPE);

    // If the audio configuration form is disabled don't register it.
    //        if ((cfg == null) || !cfg.getBoolean(AUDIO_CONFIG_DISABLED_PROP, false))
    //        {
    //            audioConfigurationForm
    //                = new LazyConfigurationForm(
    //                        AudioConfigurationPanel.class.getStatus(),
    //                        getClass().getClassLoader(),
    //                        "plugin.mediaconfig.AUDIO_ICON",
    //                        "impl.neomedia.configform.AUDIO",
    //                        3);
    //
    //            bundleContext.registerService(
    //                    ConfigurationForm.class.getStatus(),
    //                    audioConfigurationForm,
    //                    mediaProps);
    //
    //            if (deviceConfigurationPropertyChangeListener == null)
    //            {
    //                // Initializes and registers the changed device configuration
    //                // event ot the notification service.
    //                getNotificationService();
    //
    //                deviceConfigurationPropertyChangeListener
    //                    = new AudioDeviceConfigurationListener();
    //                mediaServiceImpl
    //                    .getDeviceConfiguration()
    //                        .addPropertyChangeListener(
    //                                deviceConfigurationPropertyChangeListener);
    //            }
    //        }

    // If the video configuration form is disabled don't register it.
    //        if ((cfg == null) || !cfg.getBoolean(VIDEO_CONFIG_DISABLED_PROP, false))
    //        {
    //            bundleContext.registerService(
    //                    ConfigurationForm.class.getStatus(),
    //                    new LazyConfigurationForm(
    //                            VideoConfigurationPanel.class.getStatus(),
    //                            getClass().getClassLoader(),
    //                            "plugin.mediaconfig.VIDEO_ICON",
    //                            "impl.neomedia.configform.VIDEO",
    //                            4),
    //                    mediaProps);
    //        }

    // H.264
    // If the H.264 configuration form is disabled don't register it.
    //        if ((cfg == null) || !cfg.getBoolean(H264_CONFIG_DISABLED_PROP, false))
    //        {
    //            Dictionary<String, String> h264Props
    //                = new Hashtable<String, String>();
    //
    //            h264Props.put(
    //                    ConfigurationForm.FORM_TYPE,
    //                    ConfigurationForm.ADVANCED_TYPE);
    //            bundleContext.registerService(
    //                    ConfigurationForm.class.getStatus(),
    //                    new LazyConfigurationForm(
    //                            ConfigurationPanel.class.getStatus(),
    //                            getClass().getClassLoader(),
    //                            "plugin.mediaconfig.VIDEO_ICON",
    //                            "impl.neomedia.configform.H264",
    //                            -1,
    //                            true),
    //                    h264Props);
    //        }

    // ZRTP
    // If the ZRTP configuration form is disabled don't register it.
    //        if ((cfg == null) || !cfg.getBoolean(ZRTP_CONFIG_DISABLED_PROP, false))
    //        {
    //            Dictionary<String, String> securityProps
    //                = new Hashtable<String, String>();
    //
    //            securityProps.put( ConfigurationForm.FORM_TYPE,
    //                            ConfigurationForm.SECURITY_TYPE);
    //            bundleContext.registerService(
    //                ConfigurationForm.class.getStatus(),
    //                new LazyConfigurationForm(
    //                    SecurityConfigForm.class.getStatus(),
    //                    getClass().getClassLoader(),
    //                    "impl.media.security.zrtp.CONF_ICON",
    //                    "impl.media.security.zrtp.TITLE",
    //                    0),
    //                securityProps);
    //        }

    // we use the nist-sdp stack to make parse sdp and we need to set the
    // following property to make sure that it would accept java generated
    // IPv6 addresses that contain address scope zones.
    System.setProperty("gov.nist.core.STRIP_ADDR_SCOPES", "true");

    // AudioNotifierService
    AudioNotifierService audioNotifierService = LibJitsi.getAudioNotifierService();

    audioNotifierService.setMute(
        (cfg == null)
            || !cfg.getBoolean("net.java.sip.communicator" + ".impl.sound.isSoundEnabled", true));
    bundleContext.registerService(AudioNotifierService.class.getName(), audioNotifierService, null);

    if (logger.isInfoEnabled()) logger.info("Audio Notifier Service ...[REGISTERED]");

    // Call Recording
    // If the call recording configuration form is disabled don't continue.
    //        if ((cfg == null)
    //                || !cfg.getBoolean(CALL_RECORDING_CONFIG_DISABLED_PROP, false))
    //        {
    //            Dictionary<String, String> callRecordingProps
    //                = new Hashtable<String, String>();
    //
    //            callRecordingProps.put(
    //                    ConfigurationForm.FORM_TYPE,
    //                    ConfigurationForm.ADVANCED_TYPE);
    //            bundleContext.registerService(
    //                    ConfigurationForm.class.getStatus(),
    //                    new LazyConfigurationForm(
    //                            CallRecordingConfigForm.class.getStatus(),
    //                            getClass().getClassLoader(),
    //                            null,
    //                            "plugin.callrecordingconfig.CALL_RECORDING_CONFIG",
    //                            1100,
    //                            true),
    //                    callRecordingProps);
    //        }
  }
Beispiel #12
0
  /**
   * Implements {@link ReceiveStreamListener#update(ReceiveStreamEvent)}.
   *
   * <p>{@link #rtpManager} will use this to notify us of <tt>ReceiveStreamEvent</tt>s.
   */
  @Override
  public void update(ReceiveStreamEvent event) {
    if (event == null) return;
    ReceiveStream receiveStream = event.getReceiveStream();

    if (event instanceof NewReceiveStreamEvent) {
      if (receiveStream == null) {
        logger.warn("NewReceiveStreamEvent: null");
        return;
      }

      final long ssrc = getReceiveStreamSSRC(receiveStream);

      ReceiveStreamDesc receiveStreamDesc = findReceiveStream(ssrc);

      if (receiveStreamDesc != null) {
        String s = "NewReceiveStreamEvent for an existing SSRC. ";
        if (receiveStream != receiveStreamDesc.receiveStream)
          s += "(but different ReceiveStream object)";
        logger.warn(s);
        return;
      } else receiveStreamDesc = new ReceiveStreamDesc(receiveStream);

      if (logger.isInfoEnabled()) logger.info("New ReceiveStream, ssrc=" + ssrc);

      // Find the format of the ReceiveStream
      DataSource dataSource = receiveStream.getDataSource();
      if (dataSource instanceof PushBufferDataSource) {
        Format format = null;
        PushBufferDataSource pbds = (PushBufferDataSource) dataSource;
        for (PushBufferStream pbs : pbds.getStreams()) {
          if ((format = pbs.getFormat()) != null) break;
        }

        if (format == null) {
          logger.error("Failed to handle new ReceiveStream: " + "Failed to determine format");
          return;
        }

        receiveStreamDesc.format = format;
      } else {
        logger.error("Failed to handle new ReceiveStream: " + "Unsupported DataSource");
        return;
      }

      int rtpClockRate = -1;
      if (receiveStreamDesc.format instanceof AudioFormat)
        rtpClockRate = (int) ((AudioFormat) receiveStreamDesc.format).getSampleRate();
      else if (receiveStreamDesc.format instanceof VideoFormat) rtpClockRate = 90000;
      getSynchronizer().setRtpClockRate(ssrc, rtpClockRate);

      // create a Processor and configure it
      Processor processor = null;
      try {
        processor = Manager.createProcessor(receiveStream.getDataSource());
      } catch (NoProcessorException npe) {
        logger.error("Failed to create Processor: ", npe);
        return;
      } catch (IOException ioe) {
        logger.error("Failed to create Processor: ", ioe);
        return;
      }

      if (logger.isInfoEnabled()) logger.info("Created processor for SSRC=" + ssrc);

      processor.addControllerListener(this);
      receiveStreamDesc.processor = processor;

      final int streamCount;
      synchronized (receiveStreams) {
        receiveStreams.add(receiveStreamDesc);
        streamCount = receiveStreams.size();
      }

      /*
       * XXX TODO IRBABOON
       * This is a terrible hack which works around a failure to realize()
       * some of the Processor-s for audio streams, when multiple streams
       * start nearly simultaneously. The cause of the problem is currently
       * unknown (and synchronizing all FMJ calls in RecorderRtpImpl
       * does not help).
       * XXX TODO NOOBABRI
       */
      if (receiveStreamDesc.format instanceof AudioFormat) {
        final Processor p = processor;
        new Thread() {
          @Override
          public void run() {
            // delay configuring the processors for the different
            // audio streams to decrease the probability that they
            // run together.
            try {
              int ms = 450 * (streamCount - 1);
              logger.warn(
                  "Sleeping for "
                      + ms
                      + "ms before"
                      + " configuring processor for SSRC="
                      + ssrc
                      + " "
                      + System.currentTimeMillis());
              Thread.sleep(ms);
            } catch (Exception e) {
            }

            p.configure();
          }
        }.run();
      } else {
        processor.configure();
      }
    } else if (event instanceof TimeoutEvent) {
      if (receiveStream == null) {
        // TODO: we might want to get the list of ReceiveStream-s from
        // rtpManager and compare it to our list, to see if we should
        // remove a stream.
        logger.warn("TimeoutEvent: null.");
        return;
      }

      // FMJ silently creates new ReceiveStream instances, so we have to
      // recognize them by the SSRC.
      ReceiveStreamDesc receiveStreamDesc = findReceiveStream(getReceiveStreamSSRC(receiveStream));
      if (receiveStreamDesc != null) {
        if (logger.isInfoEnabled()) {
          logger.info("ReceiveStream timeout, ssrc=" + receiveStreamDesc.ssrc);
        }

        removeReceiveStream(receiveStreamDesc, true);
      }
    } else if (event != null && logger.isInfoEnabled()) {
      logger.info("Unhandled ReceiveStreamEvent (" + event.getClass().getName() + "): " + event);
    }
  }
Beispiel #13
0
 /**
  * Log an exception.
  *
  * @param ex the exception that we are to log.
  */
 public void logException(Throwable ex) {
   logger.warn("Exception in the JAIN-SIP stack: " + ex.getMessage());
   if (logger.isInfoEnabled()) logger.info("JAIN-SIP exception stack trace is", ex);
 }
Beispiel #14
0
 /**
  * Log an info message.
  *
  * @param string the message that we'd like to log.
  */
 public void logInfo(String string) {
   if (logger.isInfoEnabled()) logger.info("Info from the JAIN-SIP stack: " + string);
 }
Beispiel #15
0
 /** {@inheritDoc} */
 @Override
 protected void doOpen() throws ResourceUnavailableException {
   if (logger.isInfoEnabled()) logger.info("Opened VP8 depacketizer");
 }
Beispiel #16
0
  /**
   * 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);
    }
  }
Beispiel #17
0
  /** {@inheritDoc} */
  @Override
  protected int doProcess(Buffer inBuffer, Buffer outBuffer) {
    byte[] inData = (byte[]) inBuffer.getData();
    int inOffset = inBuffer.getOffset();

    if (!VP8PayloadDescriptor.isValid(inData, inOffset)) {
      logger.warn("Invalid RTP/VP8 packet discarded.");
      outBuffer.setDiscard(true);
      return BUFFER_PROCESSED_FAILED; // XXX: FAILED or OK?
    }

    long inSeq = inBuffer.getSequenceNumber();
    long inRtpTimestamp = inBuffer.getRtpTimeStamp();
    int inPictureId = VP8PayloadDescriptor.getPictureId(inData, inOffset);
    boolean inMarker = (inBuffer.getFlags() & Buffer.FLAG_RTP_MARKER) != 0;
    boolean inIsStartOfFrame = VP8PayloadDescriptor.isStartOfFrame(inData, inOffset);
    int inLength = inBuffer.getLength();
    int inPdSize = VP8PayloadDescriptor.getSize(inData, inOffset);
    int inPayloadLength = inLength - inPdSize;

    if (empty && lastSentSeq != -1 && seqNumComparator.compare(inSeq, lastSentSeq) != 1) {
      if (logger.isInfoEnabled()) logger.info("Discarding old packet (while empty) " + inSeq);
      outBuffer.setDiscard(true);
      return BUFFER_PROCESSED_OK;
    }

    if (!empty) {
      // if the incoming packet has a different PictureID or timestamp
      // than those of the current frame, then it belongs to a different
      // frame.
      if ((inPictureId != -1 && pictureId != -1 && inPictureId != pictureId)
          | (timestamp != -1 && inRtpTimestamp != -1 && inRtpTimestamp != timestamp)) {
        if (seqNumComparator.compare(inSeq, firstSeq) != 1) // inSeq <= firstSeq
        {
          // the packet belongs to a previous frame. discard it
          if (logger.isInfoEnabled()) logger.info("Discarding old packet " + inSeq);
          outBuffer.setDiscard(true);
          return BUFFER_PROCESSED_OK;
        } else // inSeq > firstSeq (and also presumably isSeq > lastSeq)
        {
          // the packet belongs to a subsequent frame (to the one
          // currently being held). Drop the current frame.

          if (logger.isInfoEnabled())
            logger.info(
                "Discarding saved packets on arrival of"
                    + " a packet for a subsequent frame: "
                    + inSeq);

          // TODO: this would be the place to complain about the
          // not-well-received PictureID by sending a RTCP SLI or NACK.
          reinit();
        }
      }
    }

    // a whole frame in a single packet. avoid the extra copy to
    // this.data and output it immediately.
    if (empty && inMarker && inIsStartOfFrame) {
      byte[] outData = validateByteArraySize(outBuffer, inPayloadLength, false);
      System.arraycopy(inData, inOffset + inPdSize, outData, 0, inPayloadLength);
      outBuffer.setOffset(0);
      outBuffer.setLength(inPayloadLength);
      outBuffer.setRtpTimeStamp(inBuffer.getRtpTimeStamp());

      if (TRACE) logger.trace("Out PictureID=" + inPictureId);

      lastSentSeq = inSeq;

      return BUFFER_PROCESSED_OK;
    }

    // add to this.data
    Container container = free.poll();
    if (container == null) container = new Container();
    if (container.buf == null || container.buf.length < inPayloadLength)
      container.buf = new byte[inPayloadLength];

    if (data.get(inSeq) != null) {
      if (logger.isInfoEnabled())
        logger.info("(Probable) duplicate packet detected, discarding " + inSeq);
      outBuffer.setDiscard(true);
      return BUFFER_PROCESSED_OK;
    }

    System.arraycopy(inData, inOffset + inPdSize, container.buf, 0, inPayloadLength);
    container.len = inPayloadLength;
    data.put(inSeq, container);

    // update fields
    frameLength += inPayloadLength;
    if (firstSeq == -1 || (seqNumComparator.compare(firstSeq, inSeq) == 1)) firstSeq = inSeq;
    if (lastSeq == -1 || (seqNumComparator.compare(inSeq, lastSeq) == 1)) lastSeq = inSeq;

    if (empty) {
      // the first received packet for the current frame was just added
      empty = false;
      timestamp = inRtpTimestamp;
      pictureId = inPictureId;
    }

    if (inMarker) haveEnd = true;
    if (inIsStartOfFrame) haveStart = true;

    // check if we have a full frame
    if (frameComplete()) {
      byte[] outData = validateByteArraySize(outBuffer, frameLength, false);
      int ptr = 0;
      Container b;
      for (Map.Entry<Long, Container> entry : data.entrySet()) {
        b = entry.getValue();
        System.arraycopy(b.buf, 0, outData, ptr, b.len);
        ptr += b.len;
      }

      outBuffer.setOffset(0);
      outBuffer.setLength(frameLength);
      outBuffer.setRtpTimeStamp(inBuffer.getRtpTimeStamp());

      if (TRACE) logger.trace("Out PictureID=" + inPictureId);
      lastSentSeq = lastSeq;

      // prepare for the next frame
      reinit();

      return BUFFER_PROCESSED_OK;
    } else {
      // frame not complete yet
      outBuffer.setDiscard(true);
      return OUTPUT_BUFFER_NOT_FILLED;
    }
  }
  /**
   * Implements {@link MethodProcessor#processResponse(ResponseEvent)}. Handles only responses to
   * SUBSCRIBE requests because they are the only requests concerning event package subscribers (and
   * the only requests sent by them, for that matter) and if the processing of a given response
   * requires event package-specific handling, delivers the response to the matching
   * <tt>Subscription</tt> instance. Examples of such event package-specific handling include
   * letting the respective <tt>Subscription</tt> handle the success or failure in the establishment
   * of a subscription.
   *
   * @param responseEvent a <tt>ResponseEvent</tt> specifying the SIP <tt>Response</tt> to be
   *     processed
   * @return <tt>true</tt> if the SIP <tt>Response</tt> specified by <tt>responseEvent</tt> was
   *     processed; otherwise, <tt>false</tt>
   */
  @Override
  public boolean processResponse(ResponseEvent responseEvent) {
    Response response = responseEvent.getResponse();

    CSeqHeader cseqHeader = (CSeqHeader) response.getHeader(CSeqHeader.NAME);
    if (cseqHeader == null) {
      logger.error("An incoming response did not contain a CSeq header");
      return false;
    }
    if (!Request.SUBSCRIBE.equals(cseqHeader.getMethod())) return false;

    ClientTransaction clientTransaction = responseEvent.getClientTransaction();
    Request request = clientTransaction.getRequest();

    /*
     * Don't handle responses to requests not coming from this event
     * package.
     */
    if (request != null) {
      EventHeader eventHeader = (EventHeader) request.getHeader(EventHeader.NAME);
      if ((eventHeader == null) || !eventPackage.equalsIgnoreCase(eventHeader.getEventType()))
        return false;
    }

    // Find the subscription.
    CallIdHeader callIdHeader = (CallIdHeader) response.getHeader(CallIdHeader.NAME);
    String callId = callIdHeader.getCallId();
    Subscription subscription = getSubscription(callId);

    // if it's the response to an unsubscribe message, we just ignore it
    // whatever the response is however if we need to handle a
    // challenge, we do it
    ExpiresHeader expHeader = response.getExpires();
    int statusCode = response.getStatusCode();
    SipProvider sourceProvider = (SipProvider) responseEvent.getSource();
    if (((expHeader != null) && (expHeader.getExpires() == 0))
        || (subscription == null)) // this handle the unsubscription
    // case where we removed the contact
    // from subscribedContacts
    {
      boolean processed = false;

      if ((statusCode == Response.UNAUTHORIZED)
          || (statusCode == Response.PROXY_AUTHENTICATION_REQUIRED)) {
        try {
          processAuthenticationChallenge(clientTransaction, response, sourceProvider);
          processed = true;
        } catch (OperationFailedException e) {
          logger.error("can't handle the challenge", e);
        }
      } else if ((statusCode != Response.OK) && (statusCode != Response.ACCEPTED)) processed = true;

      // any other cases (200/202) will imply a NOTIFY, so we will
      // handle the end of a subscription there
      return processed;
    }

    if ((statusCode >= Response.OK) && (statusCode < Response.MULTIPLE_CHOICES)) {
      // OK (200/202)
      if ((statusCode == Response.OK) || (statusCode == Response.ACCEPTED)) {
        if (expHeader == null) {
          // not conform to rfc3265
          logger.error("no Expires header in this response");
          return false;
        }

        SubscriptionRefreshTask refreshTask = new SubscriptionRefreshTask(subscription);
        subscription.setTimerTask(refreshTask);

        int refreshDelay = expHeader.getExpires();
        // try to keep a margin if the refresh delay allows it
        if (refreshDelay >= (2 * refreshMargin)) refreshDelay -= refreshMargin;
        timer.schedule(refreshTask, refreshDelay * 1000);

        // do it to remember the dialog in case of a polling
        // subscription (which means no call to finalizeSubscription)
        subscription.setDialog(clientTransaction.getDialog());

        subscription.processSuccessResponse(responseEvent, statusCode);
      }
    } else if ((statusCode >= Response.MULTIPLE_CHOICES) && (statusCode < Response.BAD_REQUEST)) {
      if (logger.isInfoEnabled())
        logger.info(
            "Response to subscribe to "
                + subscription.getAddress()
                + ": "
                + response.getReasonPhrase());
    } else if (statusCode >= Response.BAD_REQUEST) {
      // if the response is a 423 response, just re-send the request
      // with a valid expires value
      if (statusCode == Response.INTERVAL_TOO_BRIEF) {
        MinExpiresHeader min = (MinExpiresHeader) response.getHeader(MinExpiresHeader.NAME);

        if (min == null) {
          logger.error("no minimal expires value in this 423 " + "response");
          return false;
        }

        ExpiresHeader exp = request.getExpires();

        try {
          exp.setExpires(min.getExpires());
        } catch (InvalidArgumentException e) {
          logger.error("can't set the new expires value", e);
          return false;
        }

        ClientTransaction transac = null;
        try {
          transac = protocolProvider.getDefaultJainSipProvider().getNewClientTransaction(request);
        } catch (TransactionUnavailableException e) {
          logger.error("can't create the client transaction", e);
          return false;
        }

        try {
          transac.sendRequest();
        } catch (SipException e) {
          logger.error("can't send the new request", e);
          return false;
        }

        return true;
        // UNAUTHORIZED (401/407)
      } else if ((statusCode == Response.UNAUTHORIZED)
          || (statusCode == Response.PROXY_AUTHENTICATION_REQUIRED)) {
        try {
          processAuthenticationChallenge(clientTransaction, response, sourceProvider);
        } catch (OperationFailedException e) {
          logger.error("can't handle the challenge", e);

          removeSubscription(callId, subscription);
          subscription.processFailureResponse(responseEvent, statusCode);
        }
        // 408 480 486 600 603 : non definitive reject
        // others: definitive reject (or not implemented)
      } else {
        if (logger.isDebugEnabled()) logger.debug("error received from the network:\n" + response);

        removeSubscription(callId, subscription);
        subscription.processFailureResponse(responseEvent, statusCode);
      }
    }

    return true;
  }