/**
   * Sets the time in milliseconds of the last activity related to this <tt>Conference</tt> to the
   * current system time.
   */
  public void touch() {
    long now = System.currentTimeMillis();

    synchronized (this) {
      if (getLastActivityTime() < now) lastActivityTime = now;
    }
  }
Пример #2
0
  /**
   * Updates rate statistics for the encodings of the tracks that this receiver is managing. Detects
   * simulcast stream suspension/resuming.
   *
   * @param pkt the {@link RawPacket} that is causing the update of this instance.
   * @param encoding the {@link RTPEncodingImpl} of the {@link RawPacket} that is passed as an
   *     argument.
   */
  void update(RawPacket pkt, RTPEncodingImpl encoding) {
    long nowMs = System.currentTimeMillis();

    // Update the encoding.
    FrameDesc frameDesc = encoding.update(pkt, nowMs);
    if (frameDesc == null /* no frame was changed */
        || !frameDesc.isIndependent() /* frame is dependent */

        // The webrtc engine is sending keyframes from high to low and less
        // often than 300 millis. The first keyframe that we observe after
        // we've waited for that long determines the streams that are
        // streaming (not suspended).
        || nowMs - lastKeyframeMs < MIN_KEY_FRAME_WAIT_MS) {
      return;
    }

    // media engines may decide to suspend a stream for congestion control.
    // This is the case with the webrtc.org simulcast implementation. This
    // behavior induces a streaming dependency between the encodings of a
    // given track. The following piece of code assumes that the subjective
    // quality array is ordered in a way to represent the streaming
    // dependencies.

    lastKeyframeMs = nowMs;
    boolean isActive = false;

    for (int i = rtpEncodings.length - 1; i >= 0; i--) {
      if (!isActive && rtpEncodings[i].requires(encoding.getIndex())) {
        isActive = true;
      }

      rtpEncodings[i].setActive(isActive);
    }
  }
    /**
     * Removes receiver and sender feedback from RTCP packets.
     *
     * @param inPacket the <tt>RTCPCompoundPacket</tt> to filter.
     * @return the filtered <tt>RawPacket</tt>.
     */
    public RawPacket gateway(RTCPCompoundPacket inPacket) {
      if (inPacket == null || inPacket.packets == null || inPacket.packets.length == 0) {
        logger.info("Ignoring empty RTCP packet.");
        return null;
      }

      ArrayList<RTCPPacket> outPackets = new ArrayList<RTCPPacket>(inPacket.packets.length);

      for (RTCPPacket p : inPacket.packets) {
        switch (p.type) {
          case RTCPPacket.RR:
          case RTCPPacket.SR:
          case RTCPPacket.SDES:
            // We generate our own RR/SR/SDES packets. We only want
            // to forward NACKs/PLIs/etc.
            break;
          case RTCPFBPacket.PSFB:
            RTCPFBPacket psfb = (RTCPFBPacket) p;
            switch (psfb.fmt) {
              case RTCPREMBPacket.FMT:
                // We generate its own REMB packets.
                break;
              default:
                // We let through everything else, like NACK
                // packets.
                outPackets.add(psfb);
                break;
            }
            break;
          default:
            // We let through everything else, like BYE and APP
            // packets.
            outPackets.add(p);
            break;
        }
      }

      if (outPackets.size() == 0) {
        return null;
      }

      // We have feedback messages to send. Pack them in a compound
      // RR and send them. TODO Use RFC5506 Reduced-Size RTCP, if the
      // receiver supports it.
      Collection<RTCPRRPacket> rrPackets = makeRTCPRRPackets(System.currentTimeMillis());

      if (rrPackets != null && rrPackets.size() != 0) {
        outPackets.addAll(0, rrPackets);
      } else {
        logger.warn("We might be sending invalid RTCPs.");
      }

      RTCPPacket[] pkts = outPackets.toArray(new RTCPPacket[outPackets.size()]);
      RTCPCompoundPacket outPacket = new RTCPCompoundPacket(pkts);

      return generator.apply(outPacket);
    }
Пример #4
0
 /**
  * Implements {@link ActiveSpeakerChangedListener#activeSpeakerChanged(long)}. Notifies this
  * <tt>RecorderRtpImpl</tt> that the audio <tt>ReceiveStream</tt> considered active has changed,
  * and that the new active stream has SSRC <tt>ssrc</tt>.
  *
  * @param ssrc the SSRC of the new active stream.
  */
 @Override
 public void activeSpeakerChanged(long ssrc) {
   if (eventHandler != null) {
     RecorderEvent e = new RecorderEvent();
     e.setAudioSsrc(ssrc);
     // TODO: how do we time this?
     e.setInstant(System.currentTimeMillis());
     e.setType(RecorderEvent.Type.SPEAKER_CHANGED);
     e.setMediaType(MediaType.VIDEO);
     eventHandler.handleEvent(e);
   }
 }
Пример #5
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);
    }
  }
 /**
  * Ctor.
  *
  * @param ssrc
  * @param remoteTime
  * @param rtpTimestamp
  * @param frequencyHz
  */
 ReceivedRemoteClock(int ssrc, long remoteTime, int rtpTimestamp, int frequencyHz) {
   this.ssrc = ssrc;
   this.remoteClock = new RemoteClock(remoteTime, rtpTimestamp);
   this.frequencyHz = frequencyHz;
   this.receivedTime = System.currentTimeMillis();
 }
  /** {@inheritDoc} */
  @Override
  public RawPacket report() {
    garbageCollector.cleanup();

    // TODO Compound RTCP packets should not exceed the MTU of the network
    // path.
    //
    // An individual RTP participant should send only one compound RTCP
    // packet per report interval in order for the RTCP bandwidth per
    // participant to be estimated correctly, except when the compound
    // RTCP packet is split for partial encryption.
    //
    // If there are too many sources to fit all the necessary RR packets
    // into one compound RTCP packet without exceeding the maximum
    // transmission unit (MTU) of the network path, then only the subset
    // that will fit into one MTU should be included in each interval. The
    // subsets should be selected round-robin across multiple intervals so
    // that all sources are reported.
    //
    // It is impossible to know in advance what the MTU of path will be.
    // There are various algorithms for experimenting to find out, but many
    // devices do not properly implement (or deliberately ignore) the
    // necessary standards so it all comes down to trial and error. For that
    // reason, we can just guess 1200 or 1500 bytes per message.
    long time = System.currentTimeMillis();

    Collection<RTCPPacket> packets = new ArrayList<RTCPPacket>();

    // First, we build the RRs.
    Collection<RTCPRRPacket> rrPackets = makeRTCPRRPackets(time);
    if (rrPackets != null && rrPackets.size() != 0) {
      packets.addAll(rrPackets);
    }

    // Next, we build the SRs.
    Collection<RTCPSRPacket> srPackets = makeRTCPSRPackets(time);
    if (srPackets != null && srPackets.size() != 0) {
      packets.addAll(srPackets);
    }

    // Bail out if we have nothing to report.
    if (packets.size() == 0) {
      return null;
    }

    // Next, we build the REMB.
    RTCPREMBPacket rembPacket = makeRTCPREMBPacket();
    if (rembPacket != null) {
      packets.add(rembPacket);
    }

    // Finally, we add an SDES packet.
    RTCPSDESPacket sdesPacket = makeSDESPacket();
    if (sdesPacket != null) {
      packets.add(sdesPacket);
    }

    // Prepare the <tt>RTCPCompoundPacket</tt> to return.
    RTCPPacket rtcpPackets[] = packets.toArray(new RTCPPacket[packets.size()]);

    RTCPCompoundPacket cp = new RTCPCompoundPacket(rtcpPackets);

    // Build the <tt>RTCPCompoundPacket</tt> and return the
    // <tt>RawPacket</tt> to inject to the <tt>MediaStream</tt>.
    return generator.apply(cp);
  }