All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jitsi.impl.neomedia.MediaStreamImpl Maven / Gradle / Ivy

/*
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jitsi.impl.neomedia;

import java.beans.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.locks.*;

import javax.media.*;
import javax.media.control.*;
import javax.media.format.*;
import javax.media.protocol.*;
import javax.media.rtp.*;
import javax.media.rtp.event.*;
import javax.media.rtp.rtcp.*;

import org.jitsi.impl.neomedia.codec.*;
import org.jitsi.impl.neomedia.codec.video.vp8.*;
import org.jitsi.impl.neomedia.device.*;
import org.jitsi.impl.neomedia.format.*;
import org.jitsi.impl.neomedia.protocol.*;
import org.jitsi.impl.neomedia.rtp.*;
import org.jitsi.impl.neomedia.rtp.remotebitrateestimator.*;
import org.jitsi.impl.neomedia.rtp.translator.*;
import org.jitsi.impl.neomedia.stats.*;
import org.jitsi.impl.neomedia.transform.*;
import org.jitsi.impl.neomedia.transform.csrc.*;
import org.jitsi.impl.neomedia.transform.dtmf.*;
import org.jitsi.impl.neomedia.transform.fec.*;
import org.jitsi.impl.neomedia.transform.pt.*;
import org.jitsi.impl.neomedia.transform.rtcp.*;
import org.jitsi.impl.neomedia.transform.zrtp.*;
import org.jitsi.service.neomedia.*;
import org.jitsi.service.neomedia.codec.*;
import org.jitsi.service.neomedia.control.*;
import org.jitsi.service.neomedia.device.*;
import org.jitsi.service.neomedia.format.*;
import org.jitsi.util.*;
import org.jitsi.utils.*;
import org.jitsi.utils.logging.*;

/**
 * Implements MediaStream using JMF.
 *
 * @author Lyubomir Marinov
 * @author Emil Ivov
 * @author Sebastien Vincent
 * @author Boris Grozev
 * @author George Politis
 */
public class MediaStreamImpl
    extends AbstractMediaStream
    implements ReceiveStreamListener,
               SendStreamListener,
               SessionListener,
               RemoteListener
{
    /**
     * The Logger used by the MediaStreamImpl class and its
     * instances for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(MediaStreamImpl.class);

    /**
     * The name of the property indicating the length of our receive buffer.
     */
    protected static final String PROPERTY_NAME_RECEIVE_BUFFER_LENGTH
        = "net.java.sip.communicator.impl.neomedia.RECEIVE_BUFFER_LENGTH";

    /**
     * Returns a human-readable representation of a specific DataSource
     * instance in the form of a String value.
     *
     * @param dataSource the DataSource to return a human-readable
     * representation of
     * @return a String value which gives a human-readable
     * representation of the specified dataSource
     */
    public static String toString(DataSource dataSource)
    {
        StringBuilder str = new StringBuilder();

        str.append(dataSource.getClass().getSimpleName());
        str.append(" with hashCode ");
        str.append(dataSource.hashCode());

        MediaLocator locator = dataSource.getLocator();

        if (locator != null)
        {
            str.append(" and locator ");
            str.append(locator);
        }
        return str.toString();
    }

    /**
     * The map of currently active RTPExtensions and the IDs that they
     * have been assigned for the lifetime of this MediaStream.
     */
    private final Map activeRTPExtensions
        = new Hashtable<>();

    /**
     * The engine that we are using in order to add CSRC lists in conference
     * calls, send CSRC sound levels, and handle incoming levels and CSRC lists.
     */
    private CsrcTransformEngine csrcEngine;

    /**
     * The session with the MediaDevice this instance uses for both
     * capture and playback of media.
     */
    private MediaDeviceSession deviceSession;

    /**
     * The PropertyChangeListener which listens to
     * {@link #deviceSession} and changes in the values of its
     * {@link MediaDeviceSession#OUTPUT_DATA_SOURCE} property.
     */
    private final PropertyChangeListener deviceSessionPropertyChangeListener
        = new PropertyChangeListener()
        {
            @Override
            public void propertyChange(PropertyChangeEvent ev)
            {
                String propertyName = ev.getPropertyName();

                if (MediaDeviceSession.OUTPUT_DATA_SOURCE.equals(propertyName))
                    deviceSessionOutputDataSourceChanged();
                else if (MediaDeviceSession.SSRC_LIST.equals(propertyName))
                    deviceSessionSsrcListChanged(ev);
            }
        };

    /**
     * The MediaDirection in which this MediaStream is allowed
     * to stream media.
     */
    private MediaDirection direction;

    /**
     * The Map of associations in this MediaStream and the
     * RTPManager it utilizes of (dynamic) RTP payload types to
     * MediaFormats.
     */
    private final Map dynamicRTPPayloadTypes
        = new HashMap<>();

    /**
     * The list of CSRC IDs contributing to the media that this
     * MediaStream is sending to its remote party.
     */
    private long[] localContributingSourceIDs;

    /**
     * Our own SSRC identifier.
     *
     * XXX(gp) how about taking the local source ID directly from
     * {@link this.rtpManager}, given that it offers this information with its
     * getLocalSSRC() method? TAG(cat4-local-ssrc-hurricane)
     */
    private long localSourceID = (new Random().nextInt()) & 0x00000000FFFFFFFFL;

    /**
     * The MediaStreamStatsImpl object used to compute the statistics about
     * this MediaStreamImpl.
     */
    private MediaStreamStats2Impl mediaStreamStatsImpl;

    /**
     * The indicator which determines whether this MediaStream is set
     * to transmit "silence" instead of the actual media fed from its
     * MediaDevice.
     */
    private boolean mute = false;

    /**
     * Number of received receiver reports. Used for logging and debugging only.
     */
    private long numberOfReceivedReceiverReports = 0;

    /**
     * Number of received sender reports. Used for logging and debugging only.
     */
    private long numberOfReceivedSenderReports = 0;

    /**
     * Engine chain overriding payload type if needed.
     */
    private PayloadTypeTransformEngine ptTransformEngine;

    /**
     * The ReceiveStreams this instance plays back on its associated
     * MediaDevice. The (read and write) accesses to the field are to
     * be synchronized using {@link #receiveStreamsLock}.
     */
    private final List receiveStreams = new LinkedList<>();

    /**
     * The ReadWriteLock which synchronizes the (read and write)
     * accesses to {@link #receiveStreams}.
     */
    private final ReadWriteLock receiveStreamsLock
        = new ReentrantReadWriteLock();

    /**
     * The SSRC identifiers of the party that we are exchanging media with.
     *
     * XXX(gp) I'm sure there's a reason why we do it the way we do it, but we
     * might want to re-think about how we manage receive SSRCs. We keep track
     * of the receive SSRC in at least 3 places, in the MediaStreamImpl (we have
     * a remoteSourceIDs vector), in StreamRTPManager.receiveSSRCs and in
     * RtpChannel.receiveSSRCs. TAG(cat4-remote-ssrc-hurricane)
     *
     */
    private final Vector remoteSourceIDs = new Vector<>(1, 1);

    /**
     * The RTPConnector through which this instance sends and receives
     * RTP and RTCP traffic. The instance is a TransformConnector in
     * order to also enable packet transformations.
     */
    private AbstractRTPConnector rtpConnector;

    /**
     * The one and only MediaStreamTarget this instance has added as a
     * target in {@link #rtpConnector}.
     */
    private MediaStreamTarget rtpConnectorTarget;

    /**
     * The RTPManager which utilizes {@link #rtpConnector} and sends
     * and receives RTP and RTCP traffic on behalf of this MediaStream.
     */
    private StreamRTPManager rtpManager;

    /**
     * The indicator which determines whether {@link #createSendStreams()} has
     * been executed for {@link #rtpManager}. If true, the
     * SendStreams have to be recreated when the MediaDevice,
     * respectively the MediaDeviceSession, of this instance is
     * changed.
     */
    protected boolean sendStreamsAreCreated = false;

    /**
     * The SrtpControl which controls the SRTP functionality of this
     * MediaStream.
     */
    private final SrtpControl srtpControl;

    /**
     * The SSRCFactory to be utilized by this instance to generate new
     * synchronization source (SSRC) identifiers. If null, this
     * instance will employ internal logic to generate new synchronization
     * source (SSRC) identifiers.
     */
    private SSRCFactory ssrcFactory = new SSRCFactoryImpl(localSourceID);

    /**
     * The indicator which determines whether {@link #start()} has been called
     * on this MediaStream without {@link #stop()} or {@link #close()}.
     */
    private boolean started = false;

    /**
     * The MediaDirection in which this instance is started. For
     * example, {@link MediaDirection#SENDRECV} if this instances is both
     * sending and receiving data (e.g. RTP and RTCP) or
     * {@link MediaDirection#SENDONLY} if this instance is only sending data.
     */
    private MediaDirection startedDirection;

    /**
     * Engine chain reading sent RTCP sender reports and stores/prints
     * statistics.
     */
    private StatisticsEngine statisticsEngine = null;

    /**
     * The TransformEngine instance that logs packets going in and out
     * of this MediaStream.
     */
    private DebugTransformEngine debugTransformEngine;

    /**
     * The TransformEngine instance registered in the
     * RTPConnector's transformer chain, which allows the "external"
     * transformer to be swapped.
     */
    private final TransformEngineWrapper
        externalTransformerWrapper
            = new TransformEngineWrapper<>();

    /**
     * The transformer which replaces the timestamp in an abs-send-time RTP
     * header extension.
     */
    private AbsSendTimeEngine absSendTimeEngine;

    /**
     * The transformer which caches outgoing RTP packets for this
     * {@link MediaStream}.
     */
    private CachingTransformer cachingTransformer = createCachingTransformer();

    /**
     * The chain used to by the RTPConnector to transform packets.
     */
    private TransformEngineChain transformEngineChain;

    /**
     * The {@code RetransmissionRequesterImpl} instance for this
     * {@code MediaStream} which will request missing packets by sending
     * RTCP NACKs.
     */
    private final RetransmissionRequesterImpl retransmissionRequester
        = createRetransmissionRequester();

    /**
     * The engine which adds an Original Header Block header extension to
     * incoming packets.
     */
    private final OriginalHeaderBlockTransformEngine ohbEngine
        = new OriginalHeaderBlockTransformEngine();

    /**
     * The {@link DiagnosticContext} that this instance provides.
     */
    private final DiagnosticContext diagnosticContext = new DiagnosticContext();

    /**
     * The ID of the frame markings RTP header extension. We use this field as
     * a cache, in order to not access {@link #activeRTPExtensions} every time.
     */
    private int frameMarkingsExtensionId = -1;

    /**
     * The {@link TransportCCEngine} instance, if any, for this
     * {@link MediaStream}. The instance could be shared between more than one
     * {@link MediaStream}, if they all use the same transport.
     */
    private TransportCCEngine transportCCEngine;

    /**
     * Initializes a new MediaStreamImpl instance which will use the
     * specified MediaDevice for both capture and playback of media.
     * The new instance will not have an associated StreamConnector and
     * it must be set later for the new instance to be able to exchange media
     * with a remote peer.
     *
     * @param device the MediaDevice the new instance is to use for
     * both capture and playback of media
     * @param srtpControl an existing control instance to control the SRTP
     * operations
     */
    public MediaStreamImpl(MediaDevice device, SrtpControl srtpControl)
    {
        this(null, device, srtpControl);
    }

    /**
     * Initializes a new MediaStreamImpl instance which will use the
     * specified MediaDevice for both capture and playback of media
     * exchanged via the specified StreamConnector.
     *
     * @param connector the StreamConnector the new instance is to use
     * for sending and receiving media or null if the
     * StreamConnector of the new instance is to not be set at
     * initialization time but specified later on
     * @param device the MediaDevice the new instance is to use for
     * both capture and playback of media exchanged via the specified
     * StreamConnector
     * @param srtpControl an existing control instance to control the ZRTP
     * operations or null if a new control instance is to be created by
     * the new MediaStreamImpl
     */
    public MediaStreamImpl(
            StreamConnector connector,
            MediaDevice device,
            SrtpControl srtpControl)
    {
        if (device != null)
        {
            /*
             * XXX Set the device early in order to make sure that it is of the
             * right type because we do not support just about any MediaDevice
             * yet.
             */
            setDevice(device);
        }

        this.srtpControl = (srtpControl == null)
            ? NeomediaServiceUtils.getMediaServiceImpl().createSrtpControl(SrtpControlType.NULL)
            : srtpControl;

        this.srtpControl.registerUser(this);
        this.mediaStreamStatsImpl = new MediaStreamStats2Impl(this);

        if (connector != null)
            setConnector(connector);

        if (logger.isTraceEnabled())
        {
            logger.trace(
                    "Created " + getClass().getSimpleName() + " with hashCode "
                        + hashCode());
        }

        diagnosticContext.put("stream", hashCode());
    }

    /**
     * Gets the {@link DiagnosticContext} of this instance.
     */
    public DiagnosticContext getDiagnosticContext()
    {
        return diagnosticContext;
    }

    /**
     * Adds a new association in this MediaStream of the specified RTP
     * payload type with the specified MediaFormat in order to allow it
     * to report rtpPayloadType in RTP flows sending and receiving
     * media in format. Usually, rtpPayloadType will be in the
     * range of dynamic RTP payload types.
     *
     * @param rtpPayloadType the RTP payload type to be associated in this
     * MediaStream with the specified MediaFormat
     * @param format the MediaFormat to be associated in this
     * MediaStream with rtpPayloadType
     * @see MediaStream#addDynamicRTPPayloadType(byte, MediaFormat)
     */
    @Override
    public void addDynamicRTPPayloadType(
            byte rtpPayloadType,
            MediaFormat format)
    {
        @SuppressWarnings("unchecked")
        MediaFormatImpl mediaFormatImpl
            = (MediaFormatImpl) format;

        synchronized (dynamicRTPPayloadTypes)
        {
            dynamicRTPPayloadTypes.put(rtpPayloadType, format);
        }

        String encoding = format.getEncoding();

        if (Constants.RED.equals(encoding))
        {
            REDTransformEngine redTransformEngine = getRedTransformEngine();
            if (redTransformEngine != null)
            {
                redTransformEngine.setIncomingPT(rtpPayloadType);
                // setting outgoingPT enables RED encapsulation for outgoing
                // packets.
                redTransformEngine.setOutgoingPT(rtpPayloadType);
            }
        }
        else if (Constants.ULPFEC.equals(encoding))
        {
            TransformEngineWrapper fecTransformEngineWrapper = getFecTransformEngine();
            if (fecTransformEngineWrapper.getWrapped() != null)
            {
                FECTransformEngine fecTransformEngine = fecTransformEngineWrapper.getWrapped();
                fecTransformEngine.setIncomingPT(rtpPayloadType);
                // TODO ULPFEC without RED doesn't make sense.
                fecTransformEngine.setOutgoingPT(rtpPayloadType);
            }
        }
        else if (Constants.FLEXFEC_03.equals(encoding))
        {
            TransformEngineWrapper fecTransformEngineWrapper = getFecTransformEngine();
            if (fecTransformEngineWrapper.getWrapped() != null)
            {
                logger.info("Updating existing FlexFEC-03 transform engine with payload type " + rtpPayloadType);
                fecTransformEngineWrapper.getWrapped().setIncomingPT(rtpPayloadType);
                fecTransformEngineWrapper.getWrapped().setOutgoingPT(rtpPayloadType);
            }
            else
            {
                logger.info("Creating FlexFEC-03 transform engine with payload type " + rtpPayloadType);
                FECTransformEngine flexFecTransformEngine =
                    new FECTransformEngine(FECTransformEngine.FecType.FLEXFEC_03,
                        rtpPayloadType, rtpPayloadType, this);
                setFecTransformEngine(flexFecTransformEngine);
            }
        }

        if (rtpManager != null)
        {
            // We do not add RED and FEC payload types to the RTP Manager
            // because RED and FEC packets will be handled before they get
            // to the RTP Manager.
            rtpManager.addFormat(
                    mediaFormatImpl.getFormat(),
                    rtpPayloadType);
        }

        this.onDynamicPayloadTypesChanged();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void clearDynamicRTPPayloadTypes()
    {
        synchronized (dynamicRTPPayloadTypes)
        {
            dynamicRTPPayloadTypes.clear();
        }

        REDTransformEngine redTransformEngine = getRedTransformEngine();
        if (redTransformEngine != null)
        {
            redTransformEngine.setIncomingPT((byte) -1);
            redTransformEngine.setOutgoingPT((byte) -1);
        }

        TransformEngineWrapper fecTransformEngineWrapper = getFecTransformEngine();
        if (fecTransformEngineWrapper != null && fecTransformEngineWrapper.getWrapped() != null)
        {
            FECTransformEngine fecTransformEngine = fecTransformEngineWrapper.getWrapped();
            fecTransformEngine.setIncomingPT((byte) -1);
            fecTransformEngine.setOutgoingPT((byte) -1);
        }

        this.onDynamicPayloadTypesChanged();
    }

    /**
     * Adds an additional RTP payload mapping that will overriding one that
     * we've set with {@link #addDynamicRTPPayloadType(byte, MediaFormat)}.
     * This is necessary so that we can support the RFC3264 case where the
     * answerer has the right to declare what payload type mappings it wants to
     * receive RTP packets with even if they are different from those in the
     * offer. RFC3264 claims this is for support of legacy protocols such as
     * H.323 but we've been bumping with a number of cases where multi-component
     * pure SIP systems also need to behave this way.
     * 

* * @param originalPt the payload type that we are overriding * @param overloadPt the payload type that we are overriding it with */ @Override public void addDynamicRTPPayloadTypeOverride(byte originalPt, byte overloadPt) { if (ptTransformEngine != null) ptTransformEngine.addPTMappingOverride(originalPt, overloadPt); } /** * Adds a specific ReceiveStream to {@link #receiveStreams}. * * @param receiveStream the ReceiveStream to add * @return true if receiveStreams changed as a result of * the method call; otherwise, false */ private boolean addReceiveStream(ReceiveStream receiveStream) { Lock writeLock = receiveStreamsLock.writeLock(); Lock readLock = receiveStreamsLock.readLock(); boolean added = false; writeLock.lock(); try { if (!receiveStreams.contains(receiveStream) && receiveStreams.add(receiveStream)) { // Downgrade the write lock to a read lock in order to allow // readers during the invocation of // MediaDeviceSession.addReceiveStream(ReceiveStream) (and // disallow writers, of course). readLock.lock(); added = true; } } finally { writeLock.unlock(); } if (added) { try { MediaDeviceSession deviceSession = getDeviceSession(); if (deviceSession == null || deviceSession.useTranslator) { // Since there is no output MediaDevice to render the // receiveStream on, the JitterBuffer of the receiveStream // will needlessly buffer and, possibly, eventually try to // adapt to the lack of free buffer space. ReceiveStreamPushBufferDataSource.setNullTransferHandler( receiveStream); } else { deviceSession.addReceiveStream(receiveStream); } } finally { readLock.unlock(); } } return added; } /** * Sets the remote SSRC identifier and fires the corresponding * PropertyChangeEvent. * * @param remoteSourceID the SSRC identifier that this stream will be using * in outgoing RTP packets from now on. */ protected void addRemoteSourceID(long remoteSourceID) { Long oldValue = getRemoteSourceID(); if(!remoteSourceIDs.contains(remoteSourceID)) remoteSourceIDs.add(remoteSourceID); firePropertyChange(PNAME_REMOTE_SSRC, oldValue, remoteSourceID); } /** * Maps or updates the mapping between extensionID and * rtpExtension. If rtpExtension's MediaDirection * attribute is set to INACTIVE the mapping is removed from the * local extensions table and the extension would not be transmitted or * handled by this stream's RTPConnector. * * @param extensionID the ID that is being mapped to rtpExtension * @param rtpExtension the RTPExtension that we are mapping. */ @Override public void addRTPExtension(byte extensionID, RTPExtension rtpExtension) { if (rtpExtension == null) return; boolean active = !MediaDirection.INACTIVE.equals(rtpExtension.getDirection()); synchronized (activeRTPExtensions) { if (active) activeRTPExtensions.put(extensionID, rtpExtension); else activeRTPExtensions.remove(extensionID); } enableRTPExtension(extensionID, rtpExtension); } /** * {@inheritDoc} */ @Override public void clearRTPExtensions() { synchronized (activeRTPExtensions) { activeRTPExtensions.clear(); frameMarkingsExtensionId = -1; if (transportCCEngine != null) { transportCCEngine.setExtensionID(-1); } if (ohbEngine != null) { ohbEngine.setExtensionID(-1); } RemoteBitrateEstimatorWrapper remoteBitrateEstimatorWrapper = getRemoteBitrateEstimator(); if (remoteBitrateEstimatorWrapper != null) { remoteBitrateEstimatorWrapper.setAstExtensionID(-1); remoteBitrateEstimatorWrapper.setTccExtensionID(-1); } if (absSendTimeEngine != null) { absSendTimeEngine.setExtensionID(-1); } } } /** * Enables all RTP extensions configured for this {@link MediaStream}. */ private void enableRTPExtensions() { synchronized (activeRTPExtensions) { for (Map.Entry entry : activeRTPExtensions.entrySet()) { enableRTPExtension(entry.getKey(), entry.getValue()); } } } /** * Enables the use of a specific RTP extension. * @param extensionID the ID. * @param rtpExtension the extension. */ private void enableRTPExtension(byte extensionID, RTPExtension rtpExtension) { boolean active = !MediaDirection.INACTIVE.equals(rtpExtension.getDirection()); int effectiveId = active ? RTPUtils.as16Bits(extensionID) : -1; String uri = rtpExtension.getURI().toString(); if (RTPExtension.ABS_SEND_TIME_URN.equals(uri)) { if (absSendTimeEngine != null) { absSendTimeEngine.setExtensionID(effectiveId); } RemoteBitrateEstimatorWrapper remoteBitrateEstimatorWrapper = getRemoteBitrateEstimator(); if (remoteBitrateEstimatorWrapper != null) { remoteBitrateEstimatorWrapper .setAstExtensionID(effectiveId); } } else if (RTPExtension.FRAME_MARKING_URN.equals(uri)) { frameMarkingsExtensionId = effectiveId; } else if (RTPExtension.ORIGINAL_HEADER_BLOCK_URN.equals(uri)) { ohbEngine.setExtensionID(effectiveId); } else if (RTPExtension.TRANSPORT_CC_URN.equals(uri)) { transportCCEngine.setExtensionID(effectiveId); RemoteBitrateEstimatorWrapper remoteBitrateEstimatorWrapper = getRemoteBitrateEstimator(); if (remoteBitrateEstimatorWrapper != null) { remoteBitrateEstimatorWrapper.setTccExtensionID(effectiveId); } } } /** * Releases the resources allocated by this instance in the course of its * execution and prepares it to be garbage collected. * * @see MediaStream#close() */ @Override public void close() { /* * Some statistics cannot be taken from the RTP manager and have to * be gathered from the ReceiveStream. We need to do this before * calling stop(). */ if(logger.isDebugEnabled()) { printReceiveStreamStatistics(); } stop(); closeSendStreams(); srtpControl.cleanup(this); if (csrcEngine != null) { csrcEngine = null; } if (cachingTransformer != null) { cachingTransformer.close(); cachingTransformer = null; } if (retransmissionRequester != null) { retransmissionRequester.close(); } if (transformEngineChain != null) { PacketTransformer t = transformEngineChain.getRTPTransformer(); if (t != null) t.close(); t = transformEngineChain.getRTCPTransformer(); if (t != null) t.close(); transformEngineChain = null; } if (transportCCEngine != null) { transportCCEngine.removeMediaStream(this); } if (rtpManager != null) { if (logger.isInfoEnabled()) printFlowStatistics(rtpManager); rtpManager.removeReceiveStreamListener(this); rtpManager.removeSendStreamListener(this); rtpManager.removeSessionListener(this); rtpManager.removeRemoteListener(this); try { rtpManager.dispose(); rtpManager = null; } catch (Throwable t) { if (t instanceof ThreadDeath) throw (ThreadDeath) t; /* * Analysis of heap dumps and application logs suggests that * RTPManager#dispose() may throw an exception after a * NullPointerException has been thrown by SendStream#close() as * documented in * #stopSendStreams(Iterable, boolean). It is * unknown at the time of this writing whether we can do * anything to prevent the exception here but it is clear that, * if we let it go through, we will not release at least one * capture device (i.e. we will at least skip the * MediaDeviceSession#close() bellow). For example, if the * exception is thrown for the audio stream in a call, its * capture device will not be released and any video stream will * not get its #close() method called at all. */ logger.error("Failed to dispose of RTPManager", t); } } /* * XXX Call AbstractRTPConnector#removeTargets() after * StreamRTPManager#dispose(). Otherwise, the latter will try to send an * RTCP BYE and there will be no targets to send it to. */ if (rtpConnector != null) rtpConnector.removeTargets(); rtpConnectorTarget = null; if (deviceSession != null) deviceSession.close(); } /** * Closes the SendStreams this instance is sending to its remote * peer. */ private void closeSendStreams() { stopSendStreams(true); } /** * Performs any optional configuration on a specific * RTPConnectorInputStream of an RTPManager to be used by * this MediaStreamImpl. Allows extenders to override. * * @param dataInputStream the RTPConnectorInputStream to be used * by an RTPManager of this MediaStreamImpl and to be * configured */ protected void configureDataInputStream( RTPConnectorInputStream dataInputStream) { dataInputStream.setPriority(getPriority()); } /** * Performs any optional configuration on a specific * RTPConnectorOuputStream of an RTPManager to be used by * this MediaStreamImpl. Allows extenders to override. * * @param dataOutputStream the RTPConnectorOutputStream to be used * by an RTPManager of this MediaStreamImpl and to be * configured */ protected void configureDataOutputStream( RTPConnectorOutputStream dataOutputStream) { dataOutputStream.setPriority(getPriority()); } /** * Performs any optional configuration on the BufferControl of the * specified RTPManager which is to be used as the * RTPManager of this MediaStreamImpl. Allows extenders to * override. * * @param rtpManager the RTPManager which is to be used by this * MediaStreamImpl * @param bufferControl the BufferControl of rtpManager on * which any optional configuration is to be performed */ protected void configureRTPManagerBufferControl( StreamRTPManager rtpManager, BufferControl bufferControl) { } /** * A stub that allows audio oriented streams to create and keep a reference * to a DtmfTransformEngine. * * @return a DtmfTransformEngine if this is an audio oriented * stream and null otherwise. */ protected DtmfTransformEngine createDtmfTransformEngine() { return null; } /** * Creates new SendStream instances for the streams of * {@link #deviceSession} through {@link #rtpManager}. */ protected void createSendStreams() { StreamRTPManager rtpManager = getRTPManager(); MediaDeviceSession deviceSession = getDeviceSession(); DataSource dataSource = deviceSession == null ? null : deviceSession.getOutputDataSource(); int streamCount; if (dataSource instanceof PushBufferDataSource) { PushBufferStream[] streams = ((PushBufferDataSource) dataSource).getStreams(); streamCount = (streams == null) ? 0 : streams.length; } else if (dataSource instanceof PushDataSource) { PushSourceStream[] streams = ((PushDataSource) dataSource).getStreams(); streamCount = (streams == null) ? 0 : streams.length; } else if (dataSource instanceof PullBufferDataSource) { PullBufferStream[] streams = ((PullBufferDataSource) dataSource).getStreams(); streamCount = (streams == null) ? 0 : streams.length; } else if (dataSource instanceof PullDataSource) { PullSourceStream[] streams = ((PullDataSource) dataSource).getStreams(); streamCount = (streams == null) ? 0 : streams.length; } else streamCount = (dataSource == null) ? 0 : 1; /* * XXX We came up with a scenario in our testing in which G.722 would * work fine for the first call since the start of the application and * then it would fail for subsequent calls, JMF would complain that the * G.722 RTP format is unknown to the RTPManager. Since * RTPManager#createSendStream(DataSource, int) is one of the cases in * which the formats registered with the RTPManager are necessary, * register them (again) just before we use them. */ registerCustomCodecFormats(rtpManager); for (int streamIndex = 0; streamIndex < streamCount; streamIndex++) { try { SendStream sendStream = rtpManager.createSendStream(dataSource, streamIndex); if (logger.isTraceEnabled()) { logger.trace( "Created SendStream with hashCode " + sendStream.hashCode() + " for " + toString(dataSource) + " and streamIndex " + streamIndex + " in RTPManager with hashCode " + rtpManager.hashCode()); } /* * JMF stores the synchronization source (SSRC) identifier as a * 32-bit signed integer, we store it as unsigned. */ long localSSRC = sendStream.getSSRC() & 0xFFFFFFFFL; if (getLocalSourceID() != localSSRC) setLocalSourceID(localSSRC); } catch (IOException ioe) { logger.error( "Failed to create send stream for data source " + dataSource + " and stream index " + streamIndex, ioe); } catch (UnsupportedFormatException ufe) { logger.error( "Failed to create send stream for data source " + dataSource + " and stream index " + streamIndex + " because of failed format " + ufe.getFailedFormat(), ufe); } } sendStreamsAreCreated = true; if (logger.isTraceEnabled()) { @SuppressWarnings("unchecked") Vector sendStreams = rtpManager.getSendStreams(); int sendStreamCount = (sendStreams == null) ? 0 : sendStreams.size(); logger.trace( "Total number of SendStreams in RTPManager with hashCode " + rtpManager.hashCode() + " is " + sendStreamCount); } } protected SsrcTransformEngine createSsrcTransformEngine() { return null; } /** * Creates the {@link AbsSendTimeEngine} for this {@code MediaStream}. * @return the created {@link AbsSendTimeEngine}. */ protected AbsSendTimeEngine createAbsSendTimeEngine() { return new AbsSendTimeEngine(); } /** * Creates the {@link CachingTransformer} for this {@code MediaStream}. * @return the created {@link CachingTransformer}. */ protected CachingTransformer createCachingTransformer() { return null; } /** * Creates the {@link RetransmissionRequesterImpl} for this * {@code MediaStream}. * @return the created {@link RetransmissionRequesterImpl}. */ protected RetransmissionRequesterImpl createRetransmissionRequester() { return null; } /** * Creates a chain of transform engines for use with this stream. Note * that this is the only place where the TransformEngineChain is * and should be manipulated to avoid problems with the order of the * transformers. * * @return the TransformEngineChain that this stream should be * using. */ private TransformEngineChain createTransformEngineChain() { List engineChain = new ArrayList<>(9); // CSRCs and CSRC audio levels if (csrcEngine == null) { csrcEngine = new CsrcTransformEngine(this); } engineChain.add(csrcEngine); // DTMF DtmfTransformEngine dtmfEngine = createDtmfTransformEngine(); if (dtmfEngine != null) { engineChain.add(dtmfEngine); } engineChain.add(externalTransformerWrapper); // RRs and REMBs. TransformEngine rtcpFeedbackTermination = getRTCPTermination(); if (rtcpFeedbackTermination != null) { engineChain.add(rtcpFeedbackTermination); } // here comes the override payload type transformer // as it changes headers of packets, need to go before encryption if (ptTransformEngine == null) ptTransformEngine = new PayloadTypeTransformEngine(); engineChain.add(ptTransformEngine); // FEC TransformEngineWrapper fecTransformEngineWrapper = getFecTransformEngine(); if (fecTransformEngineWrapper != null) { engineChain.add(fecTransformEngineWrapper); } // RED REDTransformEngine redTransformEngine = getRedTransformEngine(); if (redTransformEngine != null) engineChain.add(redTransformEngine); // RTCP Statistics if (statisticsEngine == null) statisticsEngine = new StatisticsEngine(this); engineChain.add(statisticsEngine); if (retransmissionRequester != null) { engineChain.add(retransmissionRequester); } if (cachingTransformer != null) { engineChain.add(cachingTransformer); } // Discard DiscardTransformEngine discardEngine = createDiscardEngine(); if (discardEngine != null) engineChain.add(discardEngine); MediaStreamTrackReceiver mediaStreamTrackReceiver = getMediaStreamTrackReceiver(); if (mediaStreamTrackReceiver != null) { engineChain.add(mediaStreamTrackReceiver); } // Padding termination. PaddingTermination paddingTermination = getPaddingTermination(); if (paddingTermination != null) { engineChain.add(paddingTermination); } // RTX RtxTransformer rtxTransformer = getRtxTransformer(); if (rtxTransformer != null) { engineChain.add(rtxTransformer); } // TODO RTCP termination should end up here. RemoteBitrateEstimatorWrapper remoteBitrateEstimator = getRemoteBitrateEstimator(); if (remoteBitrateEstimator != null) { engineChain.add(remoteBitrateEstimator); } absSendTimeEngine = createAbsSendTimeEngine(); if (absSendTimeEngine != null) { engineChain.add(absSendTimeEngine); } if (transportCCEngine != null) { engineChain.add(transportCCEngine.getEgressEngine()); } // Debug debugTransformEngine = DebugTransformEngine.createDebugTransformEngine(this); if (debugTransformEngine != null) engineChain.add(debugTransformEngine); // OHB engineChain.add(ohbEngine); // SRTP TransformEngine srtpTransformEngine = srtpControl.getTransformEngine(); if (srtpTransformEngine != null) { engineChain.add(srtpControl.getTransformEngine()); } if (transportCCEngine != null) { engineChain.add(transportCCEngine.getIngressEngine()); } // SSRC audio levels /* * It needs to go first in the reverse transform in order to be able to * prevent RTP packets from a muted audio source from being decrypted. */ SsrcTransformEngine ssrcEngine = createSsrcTransformEngine(); if (ssrcEngine != null) { engineChain.add(ssrcEngine); } // RTP extensions may be implemented in some of the engines just // created (e.g. abs-send-time, ohb, transport-cc). So take into // account their configuration. enableRTPExtensions(); return new TransformEngineChain( engineChain.toArray( new TransformEngine[engineChain.size()])); } /** * Notifies this MediaStream that the MediaDevice (and * respectively the MediaDeviceSession with it) which this instance * uses for capture and playback of media has been changed. Allows extenders * to override and provide additional processing of oldValue and * newValue. * * @param oldValue the MediaDeviceSession with the * MediaDevice this instance used work with * @param newValue the MediaDeviceSession with the * MediaDevice this instance is to work with */ protected void deviceSessionChanged( MediaDeviceSession oldValue, MediaDeviceSession newValue) { recreateSendStreams(); } /** * Notifies this instance that the output DataSource of its * MediaDeviceSession has changed. Recreates the * SendStreams of this instance as necessary so that it, for * example, continues streaming after the change if it was streaming before * the change. */ private void deviceSessionOutputDataSourceChanged() { recreateSendStreams(); } /** * Recalculates the list of CSRC identifiers that this MediaStream * needs to include in RTP packets bound to its interlocutor. The method * uses the list of SSRC identifiers currently handled by our device * (possibly a mixer), then removes the SSRC ID of this stream's * interlocutor. If this turns out to be the only SSRC currently in the list * we set the list of local CSRC identifiers to null since this is obviously * a non-conf call and we don't need to be advertising CSRC lists. If that's * not the case, we also add our own SSRC to the list of IDs and cache the * entire list. * * @param ev the PropetyChangeEvent containing the list of SSRC * identifiers handled by our device session before and after it changed. */ private void deviceSessionSsrcListChanged(PropertyChangeEvent ev) { long[] ssrcArray = (long[]) ev.getNewValue(); // the list is empty if(ssrcArray == null) { this.localContributingSourceIDs = null; return; } int elementsToRemove = 0; Vector remoteSourceIDs = this.remoteSourceIDs; //in case of a conf call the mixer would return all SSRC IDs that are //currently contributing including this stream's counterpart. We need //to remove that last one since that's where we will be sending our //csrc list for(int i = 0; i < ssrcArray.length; i++) { long csrc = ssrcArray[i]; if (remoteSourceIDs.contains(csrc)) elementsToRemove ++; } //we don't seem to be in a conf call since the list only contains the //SSRC id of the party that we are directly interacting with. if (elementsToRemove >= ssrcArray.length) { this.localContributingSourceIDs = null; return; } //prepare the new array. make it big enough to also add the local //SSRC id but do not make it bigger than 15 since that's the maximum //for RTP. int cc = Math.min(ssrcArray.length - elementsToRemove + 1, 15); long[] csrcArray = new long[cc]; for (int i = 0,j = 0; (i < ssrcArray.length) && (j < csrcArray.length - 1); i++) { long ssrc = ssrcArray[i]; if (!remoteSourceIDs.contains(ssrc)) { csrcArray[j] = ssrc; j++; } } csrcArray[csrcArray.length - 1] = getLocalSourceID(); this.localContributingSourceIDs = csrcArray; } /** * Sets the target of this MediaStream to which it is to send and * from which it is to receive data (e.g. RTP) and control data (e.g. RTCP). * In contrast to {@link #setTarget(MediaStreamTarget)}, sets the specified * target on this MediaStreamImpl even if its current * target is equal to the specified one. * * @param target the MediaStreamTarget describing the data * (e.g. RTP) and the control data (e.g. RTCP) locations to which this * MediaStream is to send and from which it is to receive * @see MediaStreamImpl#setTarget(MediaStreamTarget) */ private void doSetTarget(MediaStreamTarget target) { InetSocketAddress newDataAddr; InetSocketAddress newControlAddr; AbstractRTPConnector connector = rtpConnector; if (target == null) { newDataAddr = null; newControlAddr = null; } else { newDataAddr = target.getDataAddress(); newControlAddr = target.getControlAddress(); } /* * Invoke AbstractRTPConnector#removeTargets() if the new value does * actually remove an RTP or RTCP target in comparison to the old value. * If the new value is equal to the oldValue or adds an RTP or RTCP * target (i.e. the old value does not specify the respective RTP or * RTCP target and the new value does), then removeTargets is * unnecessary and would've needlessly allowed a (tiny) interval of * (execution) time (between removeTargets and addTarget) without a * target. */ if (rtpConnectorTarget != null && connector != null) { InetSocketAddress oldDataAddr = rtpConnectorTarget.getDataAddress(); boolean removeTargets = (oldDataAddr == null) ? (newDataAddr != null) : !oldDataAddr.equals(newDataAddr); if (!removeTargets) { InetSocketAddress oldControlAddr = rtpConnectorTarget.getControlAddress(); removeTargets = (oldControlAddr == null) ? (newControlAddr != null) : !oldControlAddr.equals(newControlAddr); } if (removeTargets) { connector.removeTargets(); rtpConnectorTarget = null; } } boolean targetIsSet; if (target == null || newDataAddr == null || connector == null) { targetIsSet = true; } else { try { InetAddress controlInetAddr; int controlPort; if (newControlAddr == null) { controlInetAddr = null; controlPort = 0; } else { controlInetAddr = newControlAddr.getAddress(); controlPort = newControlAddr.getPort(); } connector.addTarget( new SessionAddress( newDataAddr.getAddress(), newDataAddr.getPort(), controlInetAddr, controlPort)); targetIsSet = true; } catch (IOException ioe) { targetIsSet = false; logger.error("Failed to set target " + target, ioe); } } if (targetIsSet) { rtpConnectorTarget = target; if (logger.isTraceEnabled()) { logger.trace( "Set target of " + getClass().getSimpleName() + " with hashCode " + hashCode() + " to " + target); } } } /** * Gets the {@link TransportCCEngine} instance, if any, for this * {@link MediaStream}. The instance could be shared between more than one * {@link MediaStream}, if they all use the same transport. */ public TransportCCEngine getTransportCCEngine() { return this.transportCCEngine; } /** * Returns the ID currently assigned to a specific RTP extension. * * @param rtpExtension the RTP extension to get the currently assigned ID of * @return the ID currently assigned to the specified RTP extension or * -1 if no ID has been defined for this extension so far */ public byte getActiveRTPExtensionID(RTPExtension rtpExtension) { synchronized (activeRTPExtensions) { for (Map.Entry entry : activeRTPExtensions.entrySet()) { if (entry.getValue().equals(rtpExtension)) return entry.getKey(); } } return -1; } /** * Returns a map containing all currently active RTPExtensions in * use by this stream. * * @return a map containing all currently active RTPExtensions in * use by this stream. */ @Override public Map getActiveRTPExtensions() { synchronized (activeRTPExtensions) { return new HashMap<>(activeRTPExtensions); } } /** * Returns the engine that is responsible for adding the list of CSRC * identifiers to outgoing RTP packets during a conference. * * @return the engine that is responsible for adding the list of CSRC * identifiers to outgoing RTP packets during a conference. */ protected CsrcTransformEngine getCsrcEngine() { return csrcEngine; } /** * Gets the MediaDevice that this stream uses to play back and * capture media. * * @return the MediaDevice that this stream uses to play back and * capture media * @see MediaStream#getDevice() */ @Override public AbstractMediaDevice getDevice() { MediaDeviceSession deviceSession = getDeviceSession(); return (deviceSession == null) ? null : deviceSession.getDevice(); } /** * Gets the MediaDirection of the device of this instance. * In case there is no device, {@link MediaDirection#SENDRECV} is assumed. * * @return the MediaDirection of the device of this * instance if any or MediaDirection.SENDRECV */ private MediaDirection getDeviceDirection() { MediaDeviceSession deviceSession = getDeviceSession(); return (deviceSession == null) ? MediaDirection.SENDRECV : deviceSession.getDevice().getDirection(); } /** * Gets the MediaDeviceSession which represents the work of this * MediaStream with its associated MediaDevice. * * @return the MediaDeviceSession which represents the work of this * MediaStream with its associated MediaDevice */ public MediaDeviceSession getDeviceSession() { return deviceSession; } /** * Gets the direction in which this MediaStream is allowed to * stream media. * * @return the MediaDirection in which this MediaStream is * allowed to stream media * @see MediaStream#getDirection() */ @Override public MediaDirection getDirection() { return (direction == null) ? getDeviceDirection() : direction; } /** * Returns the payload type number that has been negotiated for the * specified encoding or -1 if no payload type has been * negotiated for it. If multiple formats match the specified * encoding, then this method would return the first one it * encounters while iterating through the map. * * @param encoding the encoding whose payload type we are trying to obtain. * * @return the payload type number that has been negotiated for the * specified encoding or -1 if no payload type has been * negotiated for it. */ public byte getDynamicRTPPayloadType(String encoding) { for (Map.Entry dynamicRTPPayloadType : getDynamicRTPPayloadTypes().entrySet()) { if (dynamicRTPPayloadType.getValue().getEncoding().equals( encoding)) { return dynamicRTPPayloadType.getKey().byteValue(); } } return -1; } /** * Gets the existing associations in this MediaStream of RTP * payload types to MediaFormats. The returned Map * only contains associations previously added in this instance with * {@link #addDynamicRTPPayloadType(byte, MediaFormat)} and not globally or * well-known associations reported by * {@link MediaFormat#getRTPPayloadType()}. * * @return a Map of RTP payload type expressed as Byte to * MediaFormat describing the existing (dynamic) associations in * this instance of RTP payload types to MediaFormats. The * Map represents a snapshot of the existing associations at the * time of the getDynamicRTPPayloadTypes() method call and * modifications to it are not reflected on the internal storage * @see MediaStream#getDynamicRTPPayloadTypes() */ @Override public Map getDynamicRTPPayloadTypes() { synchronized (dynamicRTPPayloadTypes) { return new HashMap<>(dynamicRTPPayloadTypes); } } /** * Creates the FECTransformEngine for this MediaStream. * By default none is created, allows extenders to implement it. * @return a TransformEngineWrapper around a FECTransformEngine. The * wrapper is necessary as we may not have created the {@link FECTransformEngine} * in time */ protected TransformEngineWrapper getFecTransformEngine() { return null; } /** * Sets the {@link FECTransformEngine} for this {@link MediaStream} * By default, nothing is done with the passed engine, allowing extenders * to implement it */ protected void setFecTransformEngine(FECTransformEngine fecTransformEngine) { // no op } /** * Gets the MediaFormat that this stream is currently transmitting * in. * * @return the MediaFormat that this stream is currently * transmitting in * @see MediaStream#getFormat() */ @Override public MediaFormat getFormat() { MediaDeviceSession devSess = getDeviceSession(); return (devSess == null) ? null : devSess.getFormat(); } /** * {@inheritDoc} */ @Override public MediaFormat getFormat(byte pt) { synchronized (dynamicRTPPayloadTypes) { return dynamicRTPPayloadTypes.get(pt); } } /** * Returns the list of CSRC identifiers for all parties currently known * to contribute to the media that this stream is sending toward its remote * counter part. In other words, the method returns the list of CSRC IDs * that this stream will include in outgoing RTP packets. This method will * return an null in case this stream is not part of a mixed * conference call. * * @return a long[] array of CSRC IDs representing parties that are * currently known to contribute to the media that this stream is sending * or an null in case this MediaStream is not part of a * conference call. */ public long[] getLocalContributingSourceIDs() { return localContributingSourceIDs; } /** * Gets the local address that this stream is sending RTCP traffic from. * * @return an InetSocketAddress instance indicating the local * address that this stream is sending RTCP traffic from. */ public InetSocketAddress getLocalControlAddress() { StreamConnector connector = (rtpConnector != null) ? rtpConnector.getConnector() : null; if(connector != null) { if(connector.getDataSocket() != null) { return (InetSocketAddress)connector.getControlSocket(). getLocalSocketAddress(); } else if(connector.getDataTCPSocket() != null) { return (InetSocketAddress)connector.getControlTCPSocket(). getLocalSocketAddress(); } } return null; } /** * Gets the local address that this stream is sending RTP traffic from. * * @return an InetSocketAddress instance indicating the local * address that this stream is sending RTP traffic from. */ public InetSocketAddress getLocalDataAddress() { StreamConnector connector = (rtpConnector != null) ? rtpConnector.getConnector() : null; if(connector != null) { if(connector.getDataSocket() != null) { return (InetSocketAddress)connector.getDataSocket(). getLocalSocketAddress(); } else if(connector.getDataTCPSocket() != null) { return (InetSocketAddress)connector.getDataTCPSocket(). getLocalSocketAddress(); } } return null; } /** * Gets the synchronization source (SSRC) identifier of the local peer or * -1 if it is not yet known. * * @return the synchronization source (SSRC) identifier of the local peer * or -1 if it is not yet known * @see MediaStream#getLocalSourceID() */ @Override public long getLocalSourceID() { return localSourceID; } /** * Returns the statistical information gathered about this * MediaStream. * * @return the statistical information gathered about this * MediaStream */ @Override public MediaStreamStats2Impl getMediaStreamStats() { return mediaStreamStatsImpl; } /** * Gets the MediaType of this MediaStream. * * @return the MediaType of this MediaStream */ public MediaType getMediaType() { MediaFormat format = getFormat(); MediaType mediaType = null; if (format != null) mediaType = format.getMediaType(); if (mediaType == null) { MediaDeviceSession deviceSession = getDeviceSession(); if (deviceSession != null) mediaType = deviceSession.getDevice().getMediaType(); if (mediaType == null) { if (this instanceof AudioMediaStream) mediaType = MediaType.AUDIO; else if (this instanceof VideoMediaStream) mediaType = MediaType.VIDEO; } } return mediaType; } /** * Used to set the priority of the receive/send streams. Underling * implementations can override this and return different than * current default value. * * @return the priority for the current thread. */ protected int getPriority() { return Thread.currentThread().getPriority(); } /** * Gets a ReceiveStream which this instance plays back on its * associated MediaDevice and which has a specific synchronization * source identifier (SSRC). * * @param ssrc the synchronization source identifier of the * ReceiveStream to return * @return a ReceiveStream which this instance plays back on its * associated MediaDevice and which has the specified ssrc */ public ReceiveStream getReceiveStream(int ssrc) { for (ReceiveStream receiveStream : getReceiveStreams()) { int receiveStreamSSRC = (int) receiveStream.getSSRC(); if (receiveStreamSSRC == ssrc) return receiveStream; } return null; } /** * Gets a list of the ReceiveStreams this instance plays back on * its associated MediaDevice. * * @return a list of the ReceiveStreams this instance plays back on * its associated MediaDevice */ public Collection getReceiveStreams() { Set receiveStreams = new HashSet<>(); // This instance maintains a list of the ReceiveStreams. Lock readLock = receiveStreamsLock.readLock(); readLock.lock(); try { receiveStreams.addAll(this.receiveStreams); } finally { readLock.unlock(); } /* * Unfortunately, it has been observed that sometimes there are valid * ReceiveStreams in this instance which are not returned by the * rtpManager. */ StreamRTPManager rtpManager = queryRTPManager(); if (rtpManager != null) { @SuppressWarnings("unchecked") Collection rtpManagerReceiveStreams = rtpManager.getReceiveStreams(); if (rtpManagerReceiveStreams != null) { receiveStreams.addAll(rtpManagerReceiveStreams); } } return receiveStreams; } /** * Creates the REDTransformEngine for this MediaStream. * By default none is created, allows extenders to implement it. * @return the REDTransformEngine created. */ protected REDTransformEngine getRedTransformEngine() { return null; } /** * Returns the List of CSRC identifiers representing the parties * contributing to the stream that we are receiving from this * MediaStream's remote party. * * @return a List of CSRC identifiers representing the parties * contributing to the stream that we are receiving from this * MediaStream's remote party. */ public long[] getRemoteContributingSourceIDs() { return getDeviceSession().getRemoteSSRCList(); } /** * Gets the address that this stream is sending RTCP traffic to. * * @return an InetSocketAddress instance indicating the address * that this stream is sending RTCP traffic to * @see MediaStream#getRemoteControlAddress() */ @Override public InetSocketAddress getRemoteControlAddress() { if (rtpConnector != null) { StreamConnector connector = rtpConnector.getConnector(); if (connector != null) { if (connector.getDataSocket() != null) { return (InetSocketAddress) connector .getControlSocket() .getRemoteSocketAddress(); } else if (connector.getDataTCPSocket() != null) { return (InetSocketAddress) connector .getControlTCPSocket() .getRemoteSocketAddress(); } } } return null; } /** * Gets the address that this stream is sending RTP traffic to. * * @return an InetSocketAddress instance indicating the address * that this stream is sending RTP traffic to * @see MediaStream#getRemoteDataAddress() */ @Override public InetSocketAddress getRemoteDataAddress() { StreamConnector connector = (rtpConnector != null) ? rtpConnector.getConnector() : null; if(connector != null) { if(connector.getDataSocket() != null) { return (InetSocketAddress)connector.getDataSocket(). getRemoteSocketAddress(); } else if(connector.getDataTCPSocket() != null) { return (InetSocketAddress)connector.getDataTCPSocket(). getRemoteSocketAddress(); } } return null; } /** * {@inheritDoc} * * Returns the last element of {@link #getRemoteSourceIDs()} which may or * may not always be appropriate. * * @see MediaStream#getRemoteSourceID() */ @Override public long getRemoteSourceID() { return remoteSourceIDs.isEmpty() ? -1 : remoteSourceIDs.lastElement(); } /** * Gets the synchronization source (SSRC) identifiers of the remote peer. * * @return the synchronization source (SSRC) identifiers of the remote peer */ @Override public List getRemoteSourceIDs() { /* * TODO Returning an unmodifiable view of remoteSourceIDs prevents * modifications of private state from the outside but it does not * prevent ConcurrentModificationException. */ return Collections.unmodifiableList(remoteSourceIDs); } /** * Gets the RTPConnector through which this instance sends and * receives RTP and RTCP traffic. * * @return the RTPConnector through which this instance sends and * receives RTP and RTCP traffic */ protected AbstractRTPConnector getRTPConnector() { return rtpConnector; } /** * Gets the RTPManager instance which sends and receives RTP and * RTCP traffic on behalf of this MediaStream. If the * RTPManager does not exist yet, it is created. * * @return the RTPManager instance which sends and receives RTP and * RTCP traffic on behalf of this MediaStream */ public StreamRTPManager getRTPManager() { if (rtpManager == null) { RTPConnector rtpConnector = getRTPConnector(); if (rtpConnector == null) throw new IllegalStateException("rtpConnector"); rtpManager = new StreamRTPManager(this, rtpTranslator); registerCustomCodecFormats(rtpManager); rtpManager.addReceiveStreamListener(this); rtpManager.addSendStreamListener(this); rtpManager.addSessionListener(this); rtpManager.addRemoteListener(this); BufferControl bc = rtpManager.getControl(BufferControl.class); if (bc != null) configureRTPManagerBufferControl(rtpManager, bc); rtpManager.setSSRCFactory(ssrcFactory); rtpManager.initialize(rtpConnector); // JMF initializes the local SSRC upon #initialize(RTPConnector) so // now's the time to ask. As JMF stores the SSRC as a 32-bit signed // integer value, convert it to unsigned. long localSSRC = rtpManager.getLocalSSRC(); setLocalSourceID( (localSSRC == Long.MAX_VALUE) ? -1 : (localSSRC & 0xFFFFFFFFL)); } return rtpManager; } /** * {@inheritDoc} */ @Override public StreamRTPManager getStreamRTPManager() { return queryRTPManager(); } /** * Gets the SrtpControl which controls the SRTP of this stream. * * @return the SrtpControl which controls the SRTP of this stream */ @Override public SrtpControl getSrtpControl() { return srtpControl; } /** * Returns the target of this MediaStream to which it is to send * and from which it is to receive data (e.g. RTP) and control data (e.g. * RTCP). * * @return the MediaStreamTarget describing the data * (e.g. RTP) and the control data (e.g. RTCP) locations to which this * MediaStream is to send and from which it is to receive * @see MediaStream#setTarget(MediaStreamTarget) */ @Override public MediaStreamTarget getTarget() { return rtpConnectorTarget; } /** * Returns the transport protocol used by the streams. * * @return the transport protocol (UDP or TCP) used by the streams. null if * the stream connector is not instantiated. */ @Override public StreamConnector.Protocol getTransportProtocol() { StreamConnector connector = (rtpConnector != null) ? rtpConnector.getConnector() : null; if(connector == null) { return null; } return connector.getProtocol(); } /** * Determines whether this MediaStream is set to transmit "silence" * instead of the media being fed from its MediaDevice. "Silence" * for video is understood as video data which is not the captured video * data and may represent, for example, a black image. * * @return true if this MediaStream is set to transmit * "silence" instead of the media fed from its MediaDevice; * false, otherwise * @see MediaStream#isMute() */ @Override public boolean isMute() { MediaDeviceSession deviceSession = getDeviceSession(); return (deviceSession == null) ? mute : deviceSession.isMute(); } /** * Determines whether {@link #start()} has been called on this * MediaStream without {@link #stop()} or {@link #close()} * afterwards. * * @return true if {@link #start()} has been called on this * MediaStream without {@link #stop()} or {@link #close()} * afterwards * @see MediaStream#isStarted() */ @Override public boolean isStarted() { return started; } /** * If necessary and the state of this MediaStreamImpl instance is * appropriate, updates the FMJ Formats registered with a specific * StreamRTPManager in order to possibly prevent the loss of format * parameters (i.e. SDP fmtp) specified by the remote peer and to be used * for the playback of ReceiveStreams. The Formats in * {@link #dynamicRTPPayloadTypes} will likely represent the view of the * local peer while the Format set on this MediaStream * instance will likely represent the view of the remote peer. The view of * the remote peer matters for the playback of ReceiveStreams. * * @param rtpManager the StreamRTPManager to update the registered * FMJ Formats of. If null, the method uses * {@link #rtpManager}. */ private void maybeUpdateDynamicRTPPayloadTypes(StreamRTPManager rtpManager) { if (rtpManager == null) { rtpManager = queryRTPManager(); if (rtpManager == null) return; } MediaFormat mediaFormat = getFormat(); if (!(mediaFormat instanceof MediaFormatImpl)) return; @SuppressWarnings("unchecked") MediaFormatImpl mediaFormatImpl = (MediaFormatImpl) mediaFormat; Format format = mediaFormatImpl.getFormat(); if (!(format instanceof ParameterizedVideoFormat)) return; for (Map.Entry dynamicRTPPayloadType : getDynamicRTPPayloadTypes().entrySet()) { MediaFormat dynamicMediaFormat = dynamicRTPPayloadType.getValue(); if (!(dynamicMediaFormat instanceof MediaFormatImpl)) continue; @SuppressWarnings("unchecked") MediaFormatImpl dynamicMediaFormatImpl = (MediaFormatImpl) dynamicMediaFormat; Format dynamicFormat = dynamicMediaFormatImpl.getFormat(); if (format.matches(dynamicFormat) && dynamicFormat.matches(format)) { rtpManager.addFormat( format, dynamicRTPPayloadType.getKey()); } } } /** * Prints all statistics available for {@link #rtpManager}. * * @param rtpManager the RTPManager to print statistics for */ private void printFlowStatistics(StreamRTPManager rtpManager) { try { if(!logger.isDebugEnabled()) return; //print flow statistics. GlobalTransmissionStats s = rtpManager.getGlobalTransmissionStats(); String rtpstat = StatisticsEngine.RTP_STAT_PREFIX; MediaStreamStats2Impl mss = getMediaStreamStats(); StringBuilder buff = new StringBuilder(rtpstat); MediaType mediaType = getMediaType(); String mediaTypeStr = (mediaType == null) ? "" : mediaType.toString(); String eol = "\n" + rtpstat; buff.append("call stats for outgoing ").append(mediaTypeStr) .append(" stream SSRC: ").append(getLocalSourceID()).append(eol) .append("bytes sent: ").append(s.getBytesSent()).append(eol) .append("RTP sent: ").append(s.getRTPSent()).append(eol) .append("remote reported min interarrival jitter: ") .append(mss.getMinUploadJitterMs()).append("ms").append(eol) .append("remote reported max interarrival jitter: ") .append(mss.getMaxUploadJitterMs()).append("ms").append(eol) .append("local collisions: ").append(s.getLocalColls()) .append(eol) .append("remote collisions: ").append(s.getRemoteColls()) .append(eol) .append("RTCP sent: ").append(s.getRTCPSent()).append(eol) .append("transmit failed: ").append(s.getTransmitFailed()); logger.debug(buff); GlobalReceptionStats rs = rtpManager.getGlobalReceptionStats(); MediaFormat format = getFormat(); buff = new StringBuilder(rtpstat); buff.append("call stats for incoming ") .append((format == null) ? "" : format) .append(" stream SSRC: ").append(getRemoteSourceID()) .append(eol) .append("packets received: ").append(rs.getPacketsRecd()) .append(eol) .append("bytes received: ").append(rs.getBytesRecd()) .append(eol) .append("packets lost: ") .append(mss.getReceiveStats().getPacketsLost()) .append(eol) .append("min interarrival jitter: ") .append(statisticsEngine.getMinInterArrivalJitter()) .append(eol) .append("max interarrival jitter: ") .append(statisticsEngine.getMaxInterArrivalJitter()) .append(eol) .append("RTCPs received: ").append(rs.getRTCPRecd()).append(eol) .append("bad RTCP packets: ").append(rs.getBadRTCPPkts()) .append(eol) .append("bad RTP packets: ").append(rs.getBadRTPkts()) .append(eol) .append("local collisions: ").append(rs.getLocalColls()) .append(eol) .append("malformed BYEs: ").append(rs.getMalformedBye()) .append(eol) .append("malformed RRs: ").append(rs.getMalformedRR()) .append(eol) .append("malformed SDESs: ").append(rs.getMalformedSDES()) .append(eol) .append("malformed SRs: ").append(rs.getMalformedSR()) .append(eol) .append("packets looped: ").append(rs.getPacketsLooped()) .append(eol) .append("remote collisions: ").append(rs.getRemoteColls()) .append(eol) .append("SRs received: ").append(rs.getSRRecd()).append(eol) .append("transmit failed: ").append(rs.getTransmitFailed()) .append(eol) .append("unknown types: ").append(rs.getUnknownTypes()); logger.debug(buff); } catch(Throwable t) { logger.error("Error writing statistics", t); } } private void printReceiveStreamStatistics() { mediaStreamStatsImpl.updateStats(); if (logger.isDebugEnabled()) { StringBuilder sb = new StringBuilder( "\nReceive stream stats: discarded RTP packets: ") .append(mediaStreamStatsImpl.getNbDiscarded()) .append("\nReceive stream stats: decoded with FEC: ") .append(mediaStreamStatsImpl.getNbFec()); logger.debug(sb); } } /** * Gets the RTPManager instance which sends and receives RTP and * RTCP traffic on behalf of this MediaStream. If the * RTPManager does not exist yet, it is not created. * * @return the RTPManager instance which sends and receives RTP and * RTCP traffic on behalf of this MediaStream */ public StreamRTPManager queryRTPManager() { return rtpManager; } /** * Recreates the SendStreams of this instance (i.e. of its * RTPManager) as necessary. For example, if there was no attempt * to create the SendStreams prior to the call, does nothing. If * they were created prior to the call, closes them and creates them again. * If they were not started prior to the call, does not start them after * recreating them. */ protected void recreateSendStreams() { if (sendStreamsAreCreated) { closeSendStreams(); if ((getDeviceSession() != null) && (rtpManager != null)) { if (MediaDirection.SENDONLY.equals(startedDirection) || MediaDirection.SENDRECV.equals(startedDirection)) startSendStreams(); } } } /** * Registers any custom JMF Formats with a specific * RTPManager. Extenders should override in order to register their * own customizations and should call back to this super implementation * during the execution of their override in order to register the * associations defined in this instance of (dynamic) RTP payload types to * MediaFormats. * * @param rtpManager the RTPManager to register any custom JMF * Formats with */ protected void registerCustomCodecFormats(StreamRTPManager rtpManager) { for (Map.Entry dynamicRTPPayloadType : getDynamicRTPPayloadTypes().entrySet()) { @SuppressWarnings("unchecked") MediaFormatImpl mediaFormatImpl = (MediaFormatImpl) dynamicRTPPayloadType.getValue(); Format format = mediaFormatImpl.getFormat(); rtpManager.addFormat(format, dynamicRTPPayloadType.getKey()); } maybeUpdateDynamicRTPPayloadTypes(rtpManager); } /** * Removes a specific ReceiveStream from {@link #receiveStreams}. * * @param receiveStream the ReceiveStream to remove * @return true if receiveStreams changed as a result of * the method call; otherwise, false */ private boolean removeReceiveStream(ReceiveStream receiveStream) { Lock writeLock = receiveStreamsLock.writeLock(); Lock readLock = receiveStreamsLock.readLock(); boolean removed = false; writeLock.lock(); try { if (receiveStreams.remove(receiveStream)) { /* * Downgrade the write lock to a read lock in order to allow * readers during the invocation of * MediaDeviceSession#removeReceiveStream(ReceiveStream) (and * disallow writers, of course). */ readLock.lock(); removed = true; } } finally { writeLock.unlock(); } if (removed) { try { MediaDeviceSession deviceSession = getDeviceSession(); if (deviceSession != null) deviceSession.removeReceiveStream(receiveStream); } finally { readLock.unlock(); } } return removed; } /** * {@inheritDoc} */ @Override public void removeReceiveStreamForSsrc(long ssrc) { ReceiveStream toRemove = getReceiveStream((int) ssrc); if (toRemove != null) removeReceiveStream(toRemove); // handle removal of ssrc from stats this.mediaStreamStatsImpl.removeReceiveSsrc(ssrc); } /** * Notifies this MediaStream implementation that its * RTPConnector instance has changed from a specific old value to a * specific new value. Allows extenders to override and perform additional * processing after this MediaStream has changed its * RTPConnector instance. * * @param oldValue the RTPConnector of this MediaStream * implementation before it got changed to newValue * @param newValue the current RTPConnector of this * MediaStream which replaced oldValue */ protected void rtpConnectorChanged( AbstractRTPConnector oldValue, AbstractRTPConnector newValue) { if (newValue != null) { /* * Register the transform engines that we will be using in this * stream. */ if(newValue instanceof RTPTransformUDPConnector) { transformEngineChain = createTransformEngineChain(); ((RTPTransformUDPConnector) newValue) .setEngine(transformEngineChain); } else if(newValue instanceof RTPTransformTCPConnector) { transformEngineChain = createTransformEngineChain(); ((RTPTransformTCPConnector) newValue) .setEngine(transformEngineChain); } if (rtpConnectorTarget != null) doSetTarget(rtpConnectorTarget); // Trigger the re-configuration of RTP header extensions addRTPExtension((byte)0, null); } srtpControl.setConnector(newValue); /* * TODO The following is a very ugly way to expose the RTPConnector * created by this instance so it may be configured from outside the * class hierarchy. That's why the property in use bellow is not defined * as a well-known constant and is to be considered internal and likely * to be removed in a future revision. */ try { firePropertyChange( MediaStreamImpl.class.getName() + ".rtpConnector", oldValue, newValue); } catch (Throwable t) { if (t instanceof InterruptedException) Thread.currentThread().interrupt(); else if (t instanceof ThreadDeath) throw (ThreadDeath) t; else logger.error(t); } } /** * Notifies this instance that its {@link #rtpConnector} has created a new * RTPConnectorInputStream either RTP or RTCP. * * @param inputStream the new RTPConnectorInputStream instance * created by the rtpConnector of this instance * @param data true if inputStream will be used for RTP * or false for RTCP */ private void rtpConnectorInputStreamCreated( RTPConnectorInputStream inputStream, boolean data) { /* * TODO The following is a very ugly way to expose the * RTPConnectorInputStreams created by the rtpConnector of this * instance so they may be configured from outside the class hierarchy * (e.g. to invoke addDatagramPacketFilter). That's why the property in * use bellow is not defined as a well-known constant and is to be * considered internal and likely to be removed in a future revision. */ try { firePropertyChange( MediaStreamImpl.class.getName() + ".rtpConnector." + (data ? "data" : "control") + "InputStream", null, inputStream); } catch (Throwable t) { if (t instanceof InterruptedException) Thread.currentThread().interrupt(); else if (t instanceof ThreadDeath) throw (ThreadDeath) t; else logger.error(t); } } /** * Sets the StreamConnector to be used by this instance for sending * and receiving media. * * @param connector the StreamConnector to be used by this instance * for sending and receiving media */ @Override public void setConnector(StreamConnector connector) { if (connector == null) throw new NullPointerException("connector"); AbstractRTPConnector oldValue = rtpConnector; // Is the StreamConnector really changing? if ((oldValue != null) && (oldValue.getConnector() == connector)) return; switch (connector.getProtocol()) { case UDP: rtpConnector = new RTPTransformUDPConnector(connector) { @Override protected RTPConnectorUDPInputStream createControlInputStream() throws IOException { RTPConnectorUDPInputStream s = super.createControlInputStream(); rtpConnectorInputStreamCreated(s, false); return s; } @Override protected RTPConnectorUDPInputStream createDataInputStream() throws IOException { RTPConnectorUDPInputStream s = super.createDataInputStream(); rtpConnectorInputStreamCreated(s, true); if (s != null) configureDataInputStream(s); return s; } @Override protected TransformUDPOutputStream createDataOutputStream() throws IOException { TransformUDPOutputStream s = super.createDataOutputStream(); if (s != null) configureDataOutputStream(s); return s; } }; break; case TCP: rtpConnector = new RTPTransformTCPConnector(connector) { @Override protected RTPConnectorTCPInputStream createControlInputStream() throws IOException { RTPConnectorTCPInputStream s = super.createControlInputStream(); rtpConnectorInputStreamCreated(s, false); return s; } @Override protected RTPConnectorTCPInputStream createDataInputStream() throws IOException { RTPConnectorTCPInputStream s = super.createDataInputStream(); rtpConnectorInputStreamCreated(s, true); if (s != null) configureDataInputStream(s); return s; } @Override protected TransformTCPOutputStream createDataOutputStream() throws IOException { TransformTCPOutputStream s = super.createDataOutputStream(); if (s != null) configureDataOutputStream(s); return s; } }; break; default: throw new IllegalArgumentException("connector"); } rtpConnectorChanged(oldValue, rtpConnector); } /** * Sets the MediaDevice that this stream should use to play back * and capture media. *

* Note: Also resets any previous direction set with * {@link #setDirection(MediaDirection)} to the direction of the specified * MediaDevice. * * @param device the MediaDevice that this stream should use to * play back and capture media * @see MediaStream#setDevice(MediaDevice) */ @Override public void setDevice(MediaDevice device) { if (device == null) throw new NullPointerException("device"); // Require AbstractMediaDevice for MediaDeviceSession support. AbstractMediaDevice abstractMediaDevice = (AbstractMediaDevice) device; if ((deviceSession == null) || (deviceSession.getDevice() != device)) { assertDirection(direction, device.getDirection(), "device"); MediaDeviceSession oldValue = deviceSession; MediaFormat format; MediaDirection startedDirection; if (deviceSession != null) { format = getFormat(); startedDirection = deviceSession.getStartedDirection(); deviceSession.removePropertyChangeListener( deviceSessionPropertyChangeListener); // keep player active deviceSession.setDisposePlayerOnClose( !(deviceSession instanceof VideoMediaDeviceSession)); deviceSession.close(); deviceSession = null; } else { format = null; startedDirection = MediaDirection.INACTIVE; } deviceSession = abstractMediaDevice.createSession(); /* * Copy the playback from the old MediaDeviceSession into the new * MediaDeviceSession in order to prevent the recreation of the * playback of the ReceiveStream(s) when just changing the * MediaDevice of this MediaSteam. */ if (oldValue != null) deviceSession.copyPlayback(oldValue); deviceSession.addPropertyChangeListener( deviceSessionPropertyChangeListener); /* * Setting a new device resets any previously-set direction. * Otherwise, we risk not being able to set a new device if it is * mandatory for the new device to fully cover any previously-set * direction. */ direction = null; if (deviceSession != null) { if (format != null) deviceSession.setFormat(format); deviceSession.setMute(mute); } deviceSessionChanged(oldValue, deviceSession); if (deviceSession != null) { deviceSession.start(startedDirection); // Add the receiveStreams of this instance to the new // deviceSession. Lock receiveStreamsReadLock = receiveStreamsLock.readLock(); receiveStreamsReadLock.lock(); try { for (ReceiveStream receiveStream : receiveStreams) deviceSession.addReceiveStream(receiveStream); } finally { receiveStreamsReadLock.unlock(); } } } } /** * Sets the direction in which media in this MediaStream is to be * streamed. If this MediaStream is not currently started, calls to * {@link #start()} later on will start it only in the specified * direction. If it is currently started in a direction different * than the specified, directions other than the specified will be stopped. * * @param direction the MediaDirection in which this * MediaStream is to stream media when it is started * @see MediaStream#setDirection(MediaDirection) */ @Override public void setDirection(MediaDirection direction) { if (direction == null) throw new NullPointerException("direction"); if(this.direction == direction) return; if(logger.isTraceEnabled()) { logger.trace( "Changing direction of stream " + hashCode() + " from:" + this.direction + " to:" + direction); } /* * Make sure that the specified direction is in accord with the * direction of the MediaDevice of this instance. */ assertDirection(direction, getDeviceDirection(), "direction"); this.direction = direction; switch (this.direction) { case INACTIVE: stop(MediaDirection.SENDRECV); return; case RECVONLY: stop(MediaDirection.SENDONLY); break; case SENDONLY: stop(MediaDirection.RECVONLY); break; case SENDRECV: break; default: // Don't know what it may be (in the future) so ignore it. return; } if (started) start(this.direction); // Make sure that RTP is filtered in accord with the direction of this // MediaStream, so that we don't have to worry about, for example, new // ReceiveStreams being created while in sendonly/inactive. AbstractRTPConnector connector = getRTPConnector(); if (connector != null) connector.setDirection(direction); } /** * Sets the MediaFormat that this MediaStream should * transmit in. * * @param format the MediaFormat that this MediaStream * should transmit in * @see MediaStream#setFormat(MediaFormat) */ @Override public void setFormat(MediaFormat format) { MediaDeviceSession devSess = getDeviceSession(); MediaFormatImpl thisFormat = null; if (devSess != null) { thisFormat = devSess.getFormat(); if ((thisFormat != null) && thisFormat.equals(format) && thisFormat.advancedAttributesAreEqual( thisFormat.getAdvancedAttributes(), format.getAdvancedAttributes())) { return; } } if (logger.isTraceEnabled()) { logger.trace( "Changing format of stream " + hashCode() + " from: " + thisFormat + " to: " + format); } handleAttributes(format, format.getAdvancedAttributes()); handleAttributes(format, format.getFormatParameters()); if (devSess != null) devSess.setFormat(format); maybeUpdateDynamicRTPPayloadTypes(null); } /** * Sets the local SSRC identifier and fires the corresponding * PropertyChangeEvent. * * @param localSourceID the SSRC identifier that this stream will be using * in outgoing RTP packets from now on */ protected void setLocalSourceID(long localSourceID) { if (this.localSourceID != localSourceID) { Long oldValue = this.localSourceID; this.localSourceID = localSourceID; /* * If ZRTP is used, then let it know about the SSRC of the new * SendStream. Currently, ZRTP supports only one SSRC per engine. */ TransformEngine transformEngine = srtpControl.getTransformEngine(); if (transformEngine instanceof ZRTPTransformEngine) { ((ZRTPTransformEngine) transformEngine).setOwnSSRC( getLocalSourceID()); } firePropertyChange(PNAME_LOCAL_SSRC, oldValue, this.localSourceID); } } /** * Causes this MediaStream to stop transmitting the media being fed * from this stream's MediaDevice and transmit "silence" instead. * "Silence" for video is understood as video data which is not the captured * video data and may represent, for example, a black image. * * @param mute true to have this MediaStream transmit * "silence" instead of the actual media data that it captures from its * MediaDevice; false to transmit actual media data * captured from the MediaDevice of this MediaStream * @see MediaStream#setMute(boolean) */ @Override public void setMute(boolean mute) { if (this.mute != mute) { if(logger.isTraceEnabled()) logger.trace((mute? "Muting" : "Unmuting") + " stream with hashcode " + hashCode()); this.mute = mute; MediaDeviceSession deviceSession = getDeviceSession(); if (deviceSession != null) deviceSession.setMute(this.mute); } } /** * {@inheritDoc} */ @Override public void setSSRCFactory(SSRCFactory ssrcFactory) { if (this.ssrcFactory != ssrcFactory) { this.ssrcFactory = ssrcFactory; StreamRTPManager rtpManager = this.rtpManager; RTPTranslator translator = rtpTranslator; if (rtpManager != null) rtpManager.setSSRCFactory(ssrcFactory); else if (translator instanceof RTPTranslatorImpl) ((RTPTranslatorImpl)translator).setSSRCFactory(ssrcFactory); } } /** * Sets the target of this MediaStream to which it is to send and * from which it is to receive data (e.g. RTP) and control data (e.g. RTCP). * * @param target the MediaStreamTarget describing the data * (e.g. RTP) and the control data (e.g. RTCP) locations to which this * MediaStream is to send and from which it is to receive * @see MediaStream#setTarget(MediaStreamTarget) */ @Override public void setTarget(MediaStreamTarget target) { // Short-circuit if setting the same target. if (target == null) { if (rtpConnectorTarget == null) return; } else if (target.equals(rtpConnectorTarget)) return; doSetTarget(target); } /** * Starts capturing media from this stream's MediaDevice and then * streaming it through the local StreamConnector toward the * stream's target address and port. Also puts the MediaStream in a * listening state which make it play all media received from the * StreamConnector on the stream's MediaDevice. * * @see MediaStream#start() */ @Override public void start() { start(getDirection()); started = true; } /** * Starts the processing of media in this instance in a specific direction. * * @param direction a MediaDirection value which represents the * direction of the processing of media to be started. For example, * {@link MediaDirection#SENDRECV} to start both capture and playback of * media in this instance or {@link MediaDirection#SENDONLY} to only start * the capture of media in this instance */ private void start(MediaDirection direction) { if (direction == null) throw new NullPointerException("direction"); /* * If the local peer is the focus of a conference for which it is to * perform RTP translation even without generating media to be sent, it * should create its StreamRTPManager. */ boolean getRTPManagerForRTPTranslator = true; MediaDeviceSession deviceSession = getDeviceSession(); if (direction.allowsSending() && ((startedDirection == null) || !startedDirection.allowsSending())) { /* * The startSendStreams method will be called so the getRTPManager * method will be called as part of the execution of the former. */ getRTPManagerForRTPTranslator = false; startSendStreams(); if (deviceSession != null) deviceSession.start(MediaDirection.SENDONLY); if (MediaDirection.RECVONLY.equals(startedDirection)) startedDirection = MediaDirection.SENDRECV; else if (startedDirection == null) startedDirection = MediaDirection.SENDONLY; if (logger.isInfoEnabled()) { MediaType mediaType = getMediaType(); MediaStreamStats stats = getMediaStreamStats(); logger.info( mediaType + " codec/freq: " + stats.getEncoding() + "/" + stats.getEncodingClockRate() + " Hz"); logger.info( mediaType + " remote IP/port: " + stats.getRemoteIPAddress() + "/" + stats.getRemotePort()); } } if (direction.allowsReceiving() && ((startedDirection == null) || !startedDirection.allowsReceiving())) { /* * The startReceiveStreams method will be called so the * getRTPManager method will be called as part of the execution of * the former. */ getRTPManagerForRTPTranslator = false; startReceiveStreams(); if (deviceSession != null) deviceSession.start(MediaDirection.RECVONLY); if (MediaDirection.SENDONLY.equals(startedDirection)) startedDirection = MediaDirection.SENDRECV; else if (startedDirection == null) startedDirection = MediaDirection.RECVONLY; } /* * If the local peer is the focus of a conference for which it is to * perform RTP translation even without generating media to be sent, it * should create its StreamRTPManager. */ if (getRTPManagerForRTPTranslator && (rtpTranslator != null)) getRTPManager(); } /** * Starts the ReceiveStreams that this instance is receiving from * its remote peer. By design, a MediaStream instance is associated * with a single ReceiveStream at a time. However, the * ReceiveStreams are created by RTPManager and it tracks * multiple ReceiveStreams. In practice, the RTPManager of * this MediaStreamImpl will have a single ReceiveStream * in its list. */ private void startReceiveStreams() { /* * The ReceiveStreams originate from RtpManager, make sure that there is * an actual RTPManager to initialize ReceiveStreams which are then to * be started. */ getRTPManager(); for (ReceiveStream receiveStream : getReceiveStreams()) { try { DataSource receiveStreamDataSource = receiveStream.getDataSource(); /* * For an unknown reason, the stream DataSource can be null * at the end of the Call after re-INVITEs have been * handled. */ if (receiveStreamDataSource != null) receiveStreamDataSource.start(); } catch (IOException ioex) { logger.warn( "Failed to start receive stream " + receiveStream, ioex); } } } /** * Starts the SendStreams of the RTPManager of this * MediaStreamImpl. */ private void startSendStreams() { /* * Until it's clear that the SendStreams are required (i.e. we've * negotiated to send), they will not be created. Otherwise, their * creation isn't only illogical but also causes the CaptureDevice to * be used. */ if (!sendStreamsAreCreated) createSendStreams(); StreamRTPManager rtpManager = getRTPManager(); @SuppressWarnings("unchecked") Iterable sendStreams = rtpManager.getSendStreams(); if (sendStreams != null) { for (SendStream sendStream : sendStreams) { try { DataSource sendStreamDataSource = sendStream.getDataSource(); // TODO Are we sure we want to connect here? sendStreamDataSource.connect(); sendStream.start(); sendStreamDataSource.start(); if (logger.isTraceEnabled()) { logger.trace( "Started SendStream with hashCode " + sendStream.hashCode()); } } catch (IOException ioe) { logger.warn("Failed to start stream " + sendStream, ioe); } } } } /** * Stops all streaming and capturing in this MediaStream and closes * and releases all open/allocated devices/resources. Has no effect if this * MediaStream is already closed and is simply ignored. * * @see MediaStream#stop() */ @Override public void stop() { stop(MediaDirection.SENDRECV); started = false; } /** * Stops the processing of media in this instance in a specific direction. * * @param direction a MediaDirection value which represents the * direction of the processing of media to be stopped. For example, * {@link MediaDirection#SENDRECV} to stop both capture and playback of * media in this instance or {@link MediaDirection#SENDONLY} to only stop * the capture of media in this instance */ private void stop(MediaDirection direction) { if (direction == null) throw new NullPointerException("direction"); if (rtpManager == null) return; if ((MediaDirection.SENDRECV.equals(direction) || MediaDirection.SENDONLY.equals(direction)) && (MediaDirection.SENDRECV.equals(startedDirection) || MediaDirection.SENDONLY.equals(startedDirection))) { /* * XXX It is not very clear at the time of this writing whether the * SendStreams are to be stopped or closed. On one hand, stopping a * direction may be a temporary transition which relies on retaining * the SSRC. On the other hand, it may be permanent. In which case, * the respective ReceiveStream on the remote peer will timeout at * some point in time. In the context of video conferences, when a * member stops the streaming of their video without leaving the * conference, they will stop their SendStreams. However, the other * members will need respective BYE RTCP packets in order to know * that they are to remove the associated ReceiveStreams from * display. The initial version of the code here used to stop the * SendStreams without closing them but, given the considerations * above, it is being changed to close them in the case of video. */ stopSendStreams(this instanceof VideoMediaStream); if (deviceSession != null) deviceSession.stop(MediaDirection.SENDONLY); if (MediaDirection.SENDRECV.equals(startedDirection)) startedDirection = MediaDirection.RECVONLY; else if (MediaDirection.SENDONLY.equals(startedDirection)) startedDirection = null; } if ((MediaDirection.SENDRECV.equals(direction) || MediaDirection.RECVONLY.equals(direction)) && (MediaDirection.SENDRECV.equals(startedDirection) || MediaDirection.RECVONLY.equals(startedDirection))) { stopReceiveStreams(); if (deviceSession != null) deviceSession.stop(MediaDirection.RECVONLY); if (MediaDirection.SENDRECV.equals(startedDirection)) startedDirection = MediaDirection.SENDONLY; else if (MediaDirection.RECVONLY.equals(startedDirection)) startedDirection = null; } } /** * Stops the ReceiveStreams that this instance is receiving from * its remote peer. By design, a MediaStream instance is associated * with a single ReceiveStream at a time. However, the * ReceiveStreams are created by RTPManager and it tracks * multiple ReceiveStreams. In practice, the RTPManager of * this MediaStreamImpl will have a single ReceiveStream * in its list. */ private void stopReceiveStreams() { for (ReceiveStream receiveStream : getReceiveStreams()) { try { if (logger.isTraceEnabled()) { logger.trace( "Stopping receive stream with hashcode " + receiveStream.hashCode()); } DataSource receiveStreamDataSource = receiveStream.getDataSource(); /* * For an unknown reason, the stream DataSource can be null * at the end of the Call after re-INVITEs have been * handled. */ if (receiveStreamDataSource != null) receiveStreamDataSource.stop(); } catch (IOException ioex) { logger.warn( "Failed to stop receive stream " + receiveStream, ioex); } } } /** * Stops the SendStreams that this instance is sending to its * remote peer and optionally closes them. * * @param close true to close the SendStreams that this * instance is sending to its remote peer after stopping them; * false to only stop them * @return the SendStreams which were stopped */ private Iterable stopSendStreams(boolean close) { if (rtpManager == null) return null; @SuppressWarnings("unchecked") Iterable sendStreams = rtpManager.getSendStreams(); Iterable stoppedSendStreams = stopSendStreams(sendStreams, close); if (close) sendStreamsAreCreated = false; return stoppedSendStreams; } /** * Stops specific SendStreams and optionally closes them. * * @param sendStreams the SendStreams to be stopped and optionally * closed * @param close true to close the specified SendStreams * after stopping them; false to only stop them * @return the stopped SendStreams */ private Iterable stopSendStreams( Iterable sendStreams, boolean close) { if (sendStreams == null) return null; for (SendStream sendStream : sendStreams) { try { if (logger.isTraceEnabled()) { logger.trace( "Stopping send stream with hashcode " + sendStream.hashCode()); } sendStream.getDataSource().stop(); sendStream.stop(); if (close) { try { sendStream.close(); } catch (NullPointerException npe) { /* * Sometimes com.sun.media.rtp.RTCPTransmitter#bye() may * throw NullPointerException but it does not seem to be * guaranteed because it does not happen while debugging * and stopping at a breakpoint on SendStream#close(). * One of the cases in which it appears upon call * hang-up is if we do not close the "old" SendStreams * upon reinvite(s). Though we are now closing such * SendStreams, ignore the exception here just in case * because we already ignore IOExceptions. */ logger.error( "Failed to close send stream " + sendStream, npe); } } } catch (IOException ioe) { logger.warn("Failed to stop send stream " + sendStream, ioe); } } return sendStreams; } /** * Notifies this ReceiveStreamListener that the RTPManager * it is registered with has generated an event related to a * ReceiveStream. * * @param ev the ReceiveStreamEvent which specifies the * ReceiveStream that is the cause of the event and the very type * of the event * @see ReceiveStreamListener#update(ReceiveStreamEvent) */ @Override public void update(ReceiveStreamEvent ev) { if (ev instanceof NewReceiveStreamEvent) { // XXX we might consider not adding (or not starting) new // ReceiveStreams unless this MediaStream's direction allows // receiving. ReceiveStream receiveStream = ev.getReceiveStream(); if (receiveStream != null) { long receiveStreamSSRC = 0xFFFFFFFFL & receiveStream.getSSRC(); if (logger.isTraceEnabled()) { logger.trace( "Received new ReceiveStream with ssrc " + receiveStreamSSRC); } addRemoteSourceID(receiveStreamSSRC); addReceiveStream(receiveStream); } } else if (ev instanceof TimeoutEvent) { ReceiveStream evReceiveStream = ev.getReceiveStream(); Participant participant = ev.getParticipant(); // If we recreate streams, we will already have restarted // zrtpControl. But when on the other end someone recreates his // streams, we will receive a ByeEvent (which extends TimeoutEvent) // and then we must also restart our ZRTP. This happens, for // example, when we are already in a call and the remote peer // converts his side of the call into a conference call. // if(!zrtpRestarted) // restartZrtpControl(); List receiveStreamsToRemove = new ArrayList<>(); if (evReceiveStream != null) { receiveStreamsToRemove.add(evReceiveStream); } else if (participant != null) { Collection receiveStreams = getReceiveStreams(); Collection rtpManagerReceiveStreams = rtpManager.getReceiveStreams(); for (ReceiveStream receiveStream: receiveStreams) { if(participant.equals(receiveStream.getParticipant()) && !participant.getStreams().contains( receiveStream) && !rtpManagerReceiveStreams.contains( receiveStream)) { receiveStreamsToRemove.add(receiveStream); } } } for(ReceiveStream receiveStream : receiveStreamsToRemove) { removeReceiveStream(receiveStream); // The DataSource needs to be disconnected, because otherwise // its RTPStream thread will stay alive. We do this here because // we observed that in certain situations it fails to be done // earlier. DataSource dataSource = receiveStream.getDataSource(); if (dataSource != null) dataSource.disconnect(); } } else if (ev instanceof RemotePayloadChangeEvent) { ReceiveStream receiveStream = ev.getReceiveStream(); if (receiveStream != null) { MediaDeviceSession devSess = getDeviceSession(); if (devSess != null) { TranscodingDataSource transcodingDS = devSess.getTranscodingDataSource(receiveStream); // we receive packets, streams are active // if processor in transcoding DataSource is running, we // need to recreate it by disconnect, connect and starting // again the DataSource try { if (transcodingDS != null) { transcodingDS.disconnect(); transcodingDS.connect(); transcodingDS.start(); } // as output streams of the DataSource are recreated we // need to update mixers and everything that are using // them devSess.playbackDataSourceChanged( receiveStream.getDataSource()); } catch(IOException e) { logger.error( "Error re-creating TranscodingDataSource's" + " processor!", e); } } } } } /** * Method called back in the RemoteListener to notify * listener of all RTP Remote Events.RemoteEvents are one of * ReceiverReportEvent, SenderReportEvent or RemoteCollisionEvent * * @param ev the event */ @Override public void update(RemoteEvent ev) { if(ev instanceof SenderReportEvent || ev instanceof ReceiverReportEvent) { Report report; boolean senderReport = false; if(ev instanceof SenderReportEvent) { numberOfReceivedSenderReports++; report = ((SenderReportEvent)ev).getReport(); senderReport = true; } else { numberOfReceivedReceiverReports++; report = ((ReceiverReportEvent)ev).getReport(); } Feedback feedback = null; long remoteJitter = -1; if(report.getFeedbackReports().size() > 0) { feedback = (Feedback)report.getFeedbackReports().get(0); remoteJitter = feedback.getJitter(); getMediaStreamStats().updateRemoteJitter(remoteJitter); } //Notify encoders of the percentage of packets lost by the //other side. See RFC3550 Section 6.4.1 for the interpretation of //'fraction lost' if ((feedback != null) && (getDirection() != MediaDirection.INACTIVE)) { Set plaes = null; MediaDeviceSession deviceSession = getDeviceSession(); if (deviceSession != null) plaes = deviceSession.getEncoderControls( PacketLossAwareEncoder.class); if (plaes != null && !plaes.isEmpty()) { int expectedPacketLoss = (feedback.getFractionLost() * 100) / 256; for (PacketLossAwareEncoder plae : plaes) { if (plae != null) plae.setExpectedPacketLoss(expectedPacketLoss); } } } /* * The level of logger used here is in accord with the level of * logger used in StatisticsEngine where sent reports are logged. */ if(logger.isTraceEnabled()) { // As reports are received on every 5 seconds // print every 4th packet, on every 20 seconds if((numberOfReceivedSenderReports + numberOfReceivedReceiverReports)%4 != 1) return; StringBuilder buff = new StringBuilder(StatisticsEngine.RTP_STAT_PREFIX); MediaType mediaType = getMediaType(); String mediaTypeStr = (mediaType == null) ? "" : mediaType.toString(); buff.append("Received a ") .append(senderReport ? "sender" : "receiver") .append(" report for ") .append(mediaTypeStr) .append(" stream SSRC:") .append(getLocalSourceID()) .append(" ["); if(senderReport) { buff.append("packet count:") .append(((SenderReport) report).getSenderPacketCount()) .append(", bytes:") .append(((SenderReport) report).getSenderByteCount()); } if(feedback != null) { buff.append(", interarrival jitter:") .append(remoteJitter) .append(", lost packets:").append(feedback.getNumLost()) .append(", time since previous report:") .append((int) (feedback.getDLSR() / 65.536)) .append("ms"); } buff.append(" ]"); logger.trace(buff); } } } /** * Notifies this SendStreamListener that the RTPManager it * is registered with has generated an event related to a * SendStream. * * @param ev the SendStreamEvent which specifies the * SendStream that is the cause of the event and the very type of * the event * @see SendStreamListener#update(SendStreamEvent) */ @Override public void update(SendStreamEvent ev) { if (ev instanceof NewSendStreamEvent) { /* * JMF stores the synchronization source (SSRC) identifier as a * 32-bit signed integer, we store it as unsigned. */ long localSourceID = ev.getSendStream().getSSRC() & 0xFFFFFFFFL; if (getLocalSourceID() != localSourceID) setLocalSourceID(localSourceID); } } /** * Notifies this SessionListener that the RTPManager it is * registered with has generated an event which pertains to the session as a * whole and does not belong to a ReceiveStream or a * SendStream or a remote participant necessarily. * * @param ev the SessionEvent which specifies the source and the * very type of the event * @see SessionListener#update(SessionEvent) */ @Override public void update(SessionEvent ev) { } /** * Returns the StatisticsEngine of this instance. * @return the StatisticsEngine of this instance. */ StatisticsEngine getStatisticsEngine() { return statisticsEngine; } /** * {@inheritDoc} */ public void setExternalTransformer(TransformEngine transformEngine) { externalTransformerWrapper.setWrapped(transformEngine); } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public void injectPacket(RawPacket pkt, boolean data, TransformEngine after) throws TransmissionFailedException { this.injectPacket(pkt,data, after, false); } /** * Sends a given RTP or RTCP packet to the remote peer/side. * * @param create true to create the OutputDataStream which * is to be used to write RTP data to be sent to the remote targets if it * does not exist yet; otherwise, false */ @SuppressWarnings("unchecked") public void injectPacket(RawPacket pkt, boolean data, TransformEngine after, boolean create) throws TransmissionFailedException { if (!isStarted()) { // if stream is not started do not inject packet. return; } try { if (pkt == null || pkt.getBuffer() == null) { // It's a waste of time to invoke the method with a null pkt so // disallow it. throw new NullPointerException( pkt == null ? "pkt" : "pkt.getBuffer()"); } AbstractRTPConnector rtpConnector = getRTPConnector(); if (rtpConnector == null) throw new IllegalStateException("rtpConnector"); RTPConnectorOutputStream outputStream = data ? rtpConnector.getDataOutputStream(create) : rtpConnector.getControlOutputStream(create); // We utilize TransformEngineWrapper so it is possible to have after // wrapped. Unless we wrap after, pkt will go through the whole // TransformEngine chain (which is obviously not the idea of the // caller). if (after != null) { TransformEngineWrapper wrapper; // externalTransformerWrapper wrapper = externalTransformerWrapper; if (wrapper != null && wrapper.contains(after)) { after = wrapper; } } outputStream.write( pkt.getBuffer(), pkt.getOffset(), pkt.getLength(), /* context */ after); } catch (IllegalStateException | IOException | NullPointerException e) { throw new TransmissionFailedException(e); } } /** * Utility method that determines the temporal layer index (TID) of an RTP * packet. * * @param pkt the packet from which to get the temporal layer id * * @return the TID of the packet, -1 otherwise. * * FIXME(gp) conceptually this belongs to the {@link VideoMediaStreamImpl}, * but I don't want to be obliged to cast to use this method. */ public int getTemporalID(RawPacket pkt) { if (frameMarkingsExtensionId != -1) { RawPacket.HeaderExtension fmhe = pkt.getHeaderExtension((byte) frameMarkingsExtensionId); if (fmhe != null) { return FrameMarkingHeaderExtension.getTemporalID(fmhe); } // Note that we go on and try to use the payload itself. We may want // to change this behaviour in the future, because it will give // wrong results if the payload is encrypted. } REDBlock redBlock = getPrimaryREDBlock(pkt); if (redBlock == null || redBlock.getLength() == 0) { return -1; } final byte vp8PT = getDynamicRTPPayloadType(Constants.VP8), vp9PT = getDynamicRTPPayloadType(Constants.VP9); if (redBlock.getPayloadType() == vp8PT) { return org.jitsi.impl .neomedia.codec.video.vp8.DePacketizer.VP8PayloadDescriptor .getTemporalLayerIndex( redBlock.getBuffer(), redBlock.getOffset(), redBlock.getLength()); } else if (redBlock.getPayloadType() == vp9PT) { return org.jitsi.impl .neomedia.codec.video.vp9.DePacketizer.VP9PayloadDescriptor .getTemporalLayerIndex( redBlock.getBuffer(), redBlock.getOffset(), redBlock.getLength()); } else { // XXX not implementing temporal layer detection should not break // things. return -1; } } /** * Utility method that determines the spatial layer index (SID) of an RTP * packet. * * @param pkt the RTP packet. * * @return the SID of the packet, -1 otherwise. * * FIXME(gp) conceptually this belongs to the {@link VideoMediaStreamImpl}, * but I don't want to be obliged to cast to use this method. */ public int getSpatialID(RawPacket pkt) { if (frameMarkingsExtensionId != -1) { String encoding = getFormat(pkt.getPayloadType()).getEncoding(); RawPacket.HeaderExtension fmhe = pkt.getHeaderExtension((byte) frameMarkingsExtensionId); if (fmhe != null) { return FrameMarkingHeaderExtension.getSpatialID(fmhe, encoding); } // Note that we go on and try to use the payload itself. We may want // to change this behaviour in the future, because it will give // wrong results if the payload is encrypted. } REDBlock redBlock = getPrimaryREDBlock(pkt); if (redBlock == null || redBlock.getLength() == 0) { return -1; } final byte vp9PT = getDynamicRTPPayloadType(Constants.VP9); if (redBlock.getPayloadType() == vp9PT) { return org.jitsi.impl .neomedia.codec.video.vp9.DePacketizer.VP9PayloadDescriptor .getSpatialLayerIndex( redBlock.getBuffer(), redBlock.getOffset(), redBlock.getLength()); } else { // XXX not implementing temporal layer detection should not break // things. return -1; } } /** * Returns a boolean that indicates whether or not our we're able to detect * the frame boundaries for the codec of the packet that is specified as an * argument. * * @param pkt the {@link RawPacket} that holds the RTP packet. * * @return true if we're able to detect the frame boundaries for the codec * of the packet that is specified as an argument, false otherwise. */ public boolean supportsFrameBoundaries(RawPacket pkt) { if (frameMarkingsExtensionId == -1) { REDBlock redBlock = getPrimaryREDBlock(pkt); if (redBlock != null && redBlock.getLength() != 0) { final byte vp9PT = getDynamicRTPPayloadType(Constants.VP9), vp8PT = getDynamicRTPPayloadType(Constants.VP8), pt = redBlock.getPayloadType(); return vp9PT == pt || vp8PT == pt; } else { return false; } } else { return pkt.getHeaderExtension((byte) frameMarkingsExtensionId) != null; } } /** * Utility method that determines whether or not a packet is a start of * frame. * * @param pkt raw rtp packet. * * @return true if the packet is the start of a frame, false otherwise. * * FIXME(gp) conceptually this belongs to the {@link VideoMediaStreamImpl}, * but I don't want to be obliged to cast to use this method. * */ public boolean isStartOfFrame(RawPacket pkt) { if (!RTPPacketPredicate.INSTANCE.test(pkt)) { return false; } if (frameMarkingsExtensionId != -1) { RawPacket.HeaderExtension fmhe = pkt.getHeaderExtension((byte) frameMarkingsExtensionId); if (fmhe != null) { return FrameMarkingHeaderExtension.isStartOfFrame(fmhe); } // Note that we go on and try to use the payload itself. We may want // to change this behaviour in the future, because it will give // wrong results if the payload is encrypted. } REDBlock redBlock = getPrimaryREDBlock(pkt); if (redBlock == null || redBlock.getLength() == 0) { return false; } final byte vp8PT = getDynamicRTPPayloadType(Constants.VP8), vp9PT = getDynamicRTPPayloadType(Constants.VP9); if (redBlock.getPayloadType() == vp8PT) { return org.jitsi.impl .neomedia.codec.video.vp8.DePacketizer.VP8PayloadDescriptor .isStartOfFrame(redBlock.getBuffer(), redBlock.getOffset()); } else if (redBlock.getPayloadType() == vp9PT) { return org.jitsi.impl .neomedia.codec.video.vp9.DePacketizer.VP9PayloadDescriptor .isStartOfFrame(redBlock.getBuffer(), redBlock.getOffset(), redBlock.getLength()); } else { return false; } } /** * Utility method that determines whether or not a packet is an end of * frame. * * @param pkt raw rtp packet. * * @return true if the packet is the end of a frame, false otherwise. * * FIXME(gp) conceptually this belongs to the {@link VideoMediaStreamImpl}, * but I don't want to be obliged to cast to use this method. * */ public boolean isEndOfFrame(RawPacket pkt) { if (!RTPPacketPredicate.INSTANCE.test(pkt)) { return false; } if (frameMarkingsExtensionId != -1) { RawPacket.HeaderExtension fmhe = pkt.getHeaderExtension((byte) frameMarkingsExtensionId); if (fmhe != null) { return FrameMarkingHeaderExtension.isEndOfFrame(fmhe); } // Note that we go on and try to use the payload itself. We may want // to change this behaviour in the future, because it will give // wrong results if the payload is encrypted. } REDBlock redBlock = getPrimaryREDBlock(pkt); if (redBlock == null || redBlock.getLength() == 0) { return false; } final byte vp9PT = getDynamicRTPPayloadType(Constants.VP9); if (redBlock.getPayloadType() == vp9PT) { return org.jitsi.impl .neomedia.codec.video.vp9.DePacketizer.VP9PayloadDescriptor .isEndOfFrame(redBlock.getBuffer(), redBlock.getOffset(), redBlock.getLength()); } else { return RawPacket.isPacketMarked(pkt); } } public String packetToString(RawPacket pkt) { if (pkt == null) { return "null"; } if (pkt.getPayloadType() == getDynamicRTPPayloadType(Constants.VP8)) { byte[] buf = pkt.getBuffer(); int off = pkt.getPayloadOffset(), len = pkt.getPayloadLength(); return pkt + ", " + DePacketizer.VP8PayloadDescriptor.toString(buf, off, len); } else { return pkt.toString(); } } /** * {@inheritDoc} * This is absolutely terrible, but we need a RawPacket and the method is * used from RTPTranslator, which doesn't work with RawPacket. */ public boolean isKeyFrame(byte[] buf, int off, int len) { return isKeyFrame(new RawPacket(buf, off, len)); } /** * {@inheritDoc} */ public boolean isKeyFrame(RawPacket pkt) { // XXX merge with GenericAdaptiveTrackProjectionContext.isKeyframe(). if (!RTPPacketPredicate.INSTANCE.test(pkt)) { return false; } if (frameMarkingsExtensionId != -1) { RawPacket.HeaderExtension fmhe = pkt.getHeaderExtension((byte) frameMarkingsExtensionId); if (fmhe != null) { return FrameMarkingHeaderExtension.isKeyframe(fmhe); } // Note that we go on and try to use the payload itself. We may want // to change this behaviour in the future, because it will give // wrong results if the payload is encrypted. } REDBlock redBlock = getPrimaryREDBlock(pkt); if (redBlock == null || redBlock.getLength() == 0) { return false; } final byte vp8PT = getDynamicRTPPayloadType(Constants.VP8), h264PT = getDynamicRTPPayloadType(Constants.H264); if (redBlock.getPayloadType() == vp8PT) { return org.jitsi.impl.neomedia.codec.video.vp8.DePacketizer .isKeyFrame(redBlock.getBuffer(), redBlock.getOffset(), redBlock.getLength()); } else if (redBlock.getPayloadType() == h264PT) { return org.jitsi.impl.neomedia.codec.video.h264.DePacketizer .isKeyFrame( redBlock.getBuffer(), redBlock.getOffset(), redBlock.getLength()); } else { return false; } } /** * Gets the {@link CachingTransformer} which (optionally) caches outgoing * packets for this {@link MediaStreamImpl}, if it exists. * @return the {@link CachingTransformer} for this {@link MediaStreamImpl}. */ public CachingTransformer getCachingTransformer() { return cachingTransformer; } /** * {@inheritDoc} */ public RetransmissionRequester getRetransmissionRequester() { return retransmissionRequester; } /** * {@inheritDoc} *
* Note that the chain is only initialized when a {@link StreamConnector} is * set for the {@link MediaStreamImpl} via * {@link #setConnector(StreamConnector)} or by passing a non-null connector * to the constructor. Until the chain is initialized, this method will * return null. */ @Override public TransformEngineChain getTransformEngineChain() { return transformEngineChain; } /** * {@inheritDoc} */ @Override public REDBlock getPrimaryREDBlock(ByteArrayBuffer baf) { return getPrimaryREDBlock(new RawPacket( baf.getBuffer(), baf.getOffset(), baf.getLength())); } /** * Gets the {@link REDBlock} that contains the payload of the packet passed * in as a parameter. * * @param pkt the packet from which we want to get the primary RED block * @return the {@link REDBlock} that contains the payload of the packet * passed in as a parameter, or null if the buffer is invalid. */ @Override public REDBlock getPrimaryREDBlock(RawPacket pkt) { if (pkt == null || pkt.getLength() < RawPacket.FIXED_HEADER_SIZE) { return null; } final byte redPT = getDynamicRTPPayloadType(Constants.RED), pktPT = pkt.getPayloadType(); if (redPT == pktPT) { return REDBlockIterator.getPrimaryBlock( pkt.getBuffer(), pkt.getPayloadOffset(), pkt.getPayloadLength()); } else { return new REDBlock( pkt.getBuffer(), pkt.getPayloadOffset(), pkt.getPayloadLength(), pktPT); } } /** * Gets the {@code RtxTransformer}, if any, used by the {@code MediaStream}. * * @return the {@code RtxTransformer} used by the {@code MediaStream} or * {@code null} */ public RtxTransformer getRtxTransformer() { return null; } /** * Creates the {@link DiscardTransformEngine} for this stream. Allows * extenders to override. */ protected DiscardTransformEngine createDiscardEngine() { return null; } /** * Gets the RTCP termination for this {@link MediaStreamImpl}. */ protected TransformEngine getRTCPTermination() { return null; } /** * Gets the {@link PaddingTermination} for this {@link MediaStreamImpl}. */ protected PaddingTermination getPaddingTermination() { return null; } /** * Gets the RemoteBitrateEstimator of this * VideoMediaStream. * * @return the RemoteBitrateEstimator of this * VideoMediaStream if any; otherwise, null */ public RemoteBitrateEstimatorWrapper getRemoteBitrateEstimator() { return null; } /** * Code that runs when the dynamic payload types change. */ private void onDynamicPayloadTypesChanged() { RtxTransformer rtxTransformer = getRtxTransformer(); if (rtxTransformer != null) { rtxTransformer.onDynamicPayloadTypesChanged(); } } /** * {@inheritDoc} */ @Override public void setTransportCCEngine(TransportCCEngine engine) { if (transportCCEngine != null) { transportCCEngine.removeMediaStream(this); } this.transportCCEngine = engine; if (transportCCEngine != null) { transportCCEngine.addMediaStream(this); } } /** * {@inheritDoc} */ @Override public void setRTPTranslator(RTPTranslator rtpTranslator) { super.setRTPTranslator(rtpTranslator); if (this.deviceSession != null) { this.deviceSession.setUseTranslator(rtpTranslator != null); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy