-
Lyubomir Marinov authoredLyubomir Marinov authored
AudioMediaStreamImpl.java 16.78 KiB
/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jitsi.impl.neomedia;
import java.util.*;
import javax.media.*;
import javax.media.control.*;
import javax.media.format.*;
import org.jitsi.impl.neomedia.codec.*;
import org.jitsi.impl.neomedia.device.*;
import org.jitsi.impl.neomedia.transform.dtmf.*;
import org.jitsi.service.configuration.*;
import org.jitsi.service.libjitsi.*;
import org.jitsi.service.neomedia.*;
import org.jitsi.service.neomedia.device.*;
import org.jitsi.service.neomedia.event.*;
import org.jitsi.service.protocol.*;
import org.jitsi.util.*;
/**
* Extends <tt>MediaStreamImpl</tt> in order to provide an implementation of
* <tt>AudioMediaStream</tt>.
*
* @author Lyubomir Marinov
* @author Emil Ivov
*/
public class AudioMediaStreamImpl
extends MediaStreamImpl
implements AudioMediaStream
{
/**
* The <tt>Logger</tt> used by the <tt>AudioMediaStreamImpl</tt> class and
* its instances for logging output.
*/
private static final Logger logger
= Logger.getLogger(AudioMediaStreamImpl.class);
/**
* The transformer that we use for sending and receiving DTMF packets.
*/
private DtmfTransformEngine dtmfTransfrmEngine ;
/**
* List of DTMF listeners;
*/
private List<DTMFListener> dtmfListeners = new ArrayList<DTMFListener>();
/**
* List of RTP format strings which are supported by SIP Communicator in
* addition to the JMF standard formats.
*
* @see #registerCustomCodecFormats(StreamRTPManager)
*/
private static final AudioFormat[] CUSTOM_CODEC_FORMATS
= new AudioFormat[]
{
/*
* these formats are specific, since RTP uses format numbers
* with no parameters.
*/
new AudioFormat(
Constants.ALAW_RTP,
8000,
8,
1,
Format.NOT_SPECIFIED,
AudioFormat.SIGNED),
new AudioFormat(
Constants.G722_RTP,
8000,
Format.NOT_SPECIFIED /* sampleSizeInBits */,
1)
};
/**
* The listener that gets notified of changes in the audio level of
* remote conference participants.
*/
private CsrcAudioLevelListener csrcAudioLevelListener = null;
/**
* Initializes a new <tt>AudioMediaStreamImpl</tt> instance which will use
* the specified <tt>MediaDevice</tt> for both capture and playback of audio
* exchanged via the specified <tt>StreamConnector</tt>.
*
* @param connector the <tt>StreamConnector</tt> the new instance is to use
* for sending and receiving audio
* @param device the <tt>MediaDevice</tt> the new instance is to use for
* both capture and playback of audio exchanged via the specified
* <tt>StreamConnector</tt>
* @param srtpControl a control which is already created, used to control
* the srtp operations.
*/
public AudioMediaStreamImpl(StreamConnector connector,
MediaDevice device,
SrtpControl srtpControl)
{
super(connector, device, srtpControl);
}
/**
* Performs any optional configuration on the <tt>BufferControl</tt> of the
* specified <tt>RTPManager</tt> which is to be used as the
* <tt>RTPManager</tt> of this <tt>MediaStreamImpl</tt>.
*
* @param rtpManager the <tt>RTPManager</tt> which is to be used by this
* <tt>MediaStreamImpl</tt>
* @param bufferControl the <tt>BufferControl</tt> of <tt>rtpManager</tt> on
* which any optional configuration is to be performed
*/
@Override
protected void configureRTPManagerBufferControl(
StreamRTPManager rtpManager,
BufferControl bufferControl)
{
/*
* It appears that if we don't do this managers don't play. You can try
* some other buffer size to see if you can get better smoothness.
*/
ConfigurationService cfg = LibJitsi.getConfigurationService();
long bufferLength = 100;
if (cfg != null)
{
String bufferLengthStr
= cfg.getString(PROPERTY_NAME_RECEIVE_BUFFER_LENGTH);
try
{
if ((bufferLengthStr != null) && (bufferLengthStr.length() > 0))
bufferLength = Long.parseLong(bufferLengthStr);
}
catch (NumberFormatException nfe)
{
logger.warn(
bufferLengthStr
+ " is not a valid receive buffer length/long value",
nfe);
}
}
bufferLength = bufferControl.setBufferLength(bufferLength);
if (logger.isTraceEnabled())
logger.trace("Set receiver buffer length to " + bufferLength);
bufferControl.setEnabledThreshold(true);
bufferControl.setMinimumThreshold(100);
}
/**
* A stub that allows audio oriented streams to create and keep a reference
* to a <tt>DtmfTransformEngine</tt>.
*
* @return a <tt>DtmfTransformEngine</tt> if this is an audio oriented
* stream and <tt>null</tt> otherwise.
*/
@Override
protected DtmfTransformEngine createDtmfTransformEngine()
{
if(this.dtmfTransfrmEngine == null)
this.dtmfTransfrmEngine = new DtmfTransformEngine(this);
return this.dtmfTransfrmEngine;
}
/**
* Adds a <tt>DTMFListener</tt> to this <tt>AudioMediaStream</tt> which is
* to receive notifications when the remote party starts sending DTMF tones
* to us.
*
* @param listener the <tt>DTMFListener</tt> to register for notifications
* about the remote party starting sending of DTM tones to this
* <tt>AudioMediaStream</tt>
* @see AudioMediaStream#addDTMFListener(DTMFListener)
*/
public void addDTMFListener(DTMFListener listener)
{
if(!dtmfListeners.contains(listener))
{
dtmfListeners.add(listener);
}
}
/**
* Sets <tt>listener</tt> as the <tt>SimpleAudioLevelListener</tt>
* registered to receive notifications from our device session for changes
* in the levels of the party that's at the other end of this stream.
*
* @param listener the <tt>SimpleAudioLevelListener</tt> that we'd like to
* register or <tt>null</tt> if we want to stop stream audio level
* measurements.
*/
public void setStreamAudioLevelListener(SimpleAudioLevelListener listener)
{
getDeviceSession().setStreamAudioLevelListener(listener);
}
/**
* Registers <tt>listener</tt> as the <tt>CsrcAudioLevelListener</tt> that
* will receive notifications for changes in the levels of conference
* participants that the remote party could be mixing.
*
* @param listener the <tt>CsrcAudioLevelListener</tt> that we'd like to
* register or <tt>null</tt> if we'd like to stop receiving notifications.
*/
public void setCsrcAudioLevelListener(CsrcAudioLevelListener listener)
{
this.csrcAudioLevelListener = listener;
}
/**
* Registers {@link #CUSTOM_CODEC_FORMATS} with a specific
* <tt>RTPManager</tt>.
*
* @param rtpManager the <tt>RTPManager</tt> to register
* {@link #CUSTOM_CODEC_FORMATS} with
* @see MediaStreamImpl#registerCustomCodecFormats(StreamRTPManager)
*/
@Override
protected void registerCustomCodecFormats(StreamRTPManager rtpManager)
{
super.registerCustomCodecFormats(rtpManager);
for (AudioFormat format : CUSTOM_CODEC_FORMATS)
{
if (logger.isDebugEnabled())
logger.debug("registering format " + format +
" with RTP manager");
/*
* NOTE (mkoch@rowa.de): com.sun.media.rtp.RtpSessionMgr.addFormat
* leaks memory, since it stores the Format in a static Vector.
* AFAIK there is no easy way around it, but the memory impact
* should not be too bad.
*/
rtpManager.addFormat( format,
MediaUtils.getRTPPayloadType(
format.getEncoding(), format.getSampleRate()));
}
}
/**
* Removes <tt>listener</tt> from the list of <tt>DTMFListener</tt>s
* registered with this <tt>AudioMediaStream</tt> to receive notifications
* about incoming DTMF tones.
*
* @param listener the <tt>DTMFListener</tt> to no longer be notified by
* this <tt>AudioMediaStream</tt> about incoming DTMF tones
* @see AudioMediaStream#removeDTMFListener(DTMFListener)
*/
public void removeDTMFListener(DTMFListener listener)
{
dtmfListeners.remove(listener);
}
/**
* Starts sending the specified <tt>DTMFTone</tt> until the
* <tt>stopSendingDTMF()</tt> method is called (Excepts for INBAND DTMF,
* which stops by itself this is why where there is no need to call the
* stopSendingDTMF). Callers should keep in mind the fact that calling this
* method would most likely interrupt all audio transmission until the
* corresponding stop method is called. Also, calling this method
* successively without invoking the corresponding stop method between the
* calls will simply replace the <tt>DTMFTone</tt> from the first call with
* that from the second.
*
* @param tone the <tt>DTMFTone</tt> to start sending.
* @param dtmfMethod The kind of DTMF used (RTP, SIP-INOF or INBAND).
* @throws IllegalArgumentException if <tt>dtmfMethod</tt> is not one of
* {@link DTMFMethod#INBAND_DTMF}, {@link DTMFMethod#RTP_DTMF}, and
* {@link DTMFMethod#SIP_INFO_DTMF}
* @see AudioMediaStream#startSendingDTMF(DTMFTone, DTMFMethod)
*/
public void startSendingDTMF(DTMFTone tone, DTMFMethod dtmfMethod)
{
switch (dtmfMethod)
{
case INBAND_DTMF:
MediaDeviceSession deviceSession = getDeviceSession();
if (deviceSession != null)
deviceSession.addDTMF(DTMFInbandTone.mapTone(tone));
break;
case RTP_DTMF:
if (dtmfTransfrmEngine != null)
{
DTMFRtpTone t = DTMFRtpTone.mapTone(tone);
if (t != null)
dtmfTransfrmEngine.startSending(t);
}
break;
case SIP_INFO_DTMF:
// This kind of DTMF is not managed directly by the
// OperationSetDTMFSipImpl.
break;
default:
throw new IllegalArgumentException("dtmfMethod");
}
}
/**
* Interrupts transmission of a <tt>DTMFTone</tt> started with the
* <tt>startSendingDTMF()</tt> method. Has no effect if no tone is currently
* being sent.
*
* @param dtmfMethod The kind of DTMF used (RTP, SIP-INOF or INBAND).
* @throws IllegalArgumentException if <tt>dtmfMethod</tt> is not one of
* {@link DTMFMethod#INBAND_DTMF}, {@link DTMFMethod#RTP_DTMF}, and
* {@link DTMFMethod#SIP_INFO_DTMF}
* @see AudioMediaStream#stopSendingDTMF(DTMFMethod)
*/
public void stopSendingDTMF(DTMFMethod dtmfMethod)
{
switch (dtmfMethod)
{
case INBAND_DTMF:
// The INBAND DTMF is send by impluse of constant duration and
// does not need to be stopped explecitely.
break;
case RTP_DTMF:
if(dtmfTransfrmEngine != null)
dtmfTransfrmEngine.stopSendingDTMF();
break;
case SIP_INFO_DTMF:
// The SIP-INFO DTMF is not managed directly by the
// OperationSetDTMFSipImpl.
break;
default:
throw new IllegalArgumentException("dtmfMethod");
}
}
/**
* In addition to calling
* {@link MediaStreamImpl#addRTPExtension(byte, RTPExtension)}
* this method enables sending of CSRC audio levels. The reason we are
* doing this here rather than in the super class is that CSRC levels only
* make sense for audio streams so we don't want them enabled in any other
* kind.
*
* @param extensionID the ID assigned to <tt>rtpExtension</tt> for the
* lifetime of this stream.
* @param rtpExtension the RTPExtension that is being added to this stream.
*/
@Override
public void addRTPExtension(byte extensionID, RTPExtension rtpExtension)
{
super.addRTPExtension(extensionID, rtpExtension);
if (RTPExtension.CSRC_AUDIO_LEVEL_URN.equals(
rtpExtension.getURI().toString()))
{
getCsrcEngine().setCsrcAudioLevelAudioLevelExtensionID(
extensionID,
rtpExtension.getDirection());
}
}
/**
* Sets <tt>listener</tt> as the <tt>SimpleAudioLevelListener</tt>
* registered to receive notifications from our device session for changes
* in the levels of the audio that this stream is sending out.
*
* @param listener the <tt>SimpleAudioLevelListener</tt> that we'd like to
* register or <tt>null</tt> if we want to stop local audio level
* measurements.
*/
public void setLocalUserAudioLevelListener(
SimpleAudioLevelListener listener)
{
getDeviceSession().setLocalUserAudioLevelListener(listener);
}
/**
* Returns the <tt>MediaDeviceSession</tt> associated with this stream
* after first casting it to <tt>AudioMediaDeviceSession</tt> since this is,
* after all, an <tt>AudioMediaStreamImpl</tt>.
*
* @return the <tt>AudioMediaDeviceSession</tt> associated with this stream.
*/
@Override
public AudioMediaDeviceSession getDeviceSession()
{
return (AudioMediaDeviceSession)super.getDeviceSession();
}
/**
* Returns the last audio level that was measured by the underlying device
* session for the specified <tt>ssrc</tt> (where <tt>ssrc</tt> could also
* correspond to our local sync source identifier).
*
* @param ssrc the SSRC ID whose last measured audio level we'd like to
* retrieve.
*
* @return the audio level that was last measured for the specified
* <tt>ssrc</tt> or <tt>-1</tt> if no level has been cached for that ID.
*/
public int getLastMeasuredAudioLevel(long ssrc)
{
AudioMediaDeviceSession devSession = getDeviceSession();
if (devSession == null)
return -1;
if ( ssrc == getLocalSourceID() )
return devSession.getLastMeasuredLocalUserAudioLevel();
else
return devSession.getLastMeasuredAudioLevel(ssrc);
}
/**
* Delivers the <tt>audioLevels</tt> map to whoever's interested. This
* method is meant for use primarily by the transform engine handling
* incoming RTP packets (currently <tt>CsrcTransformEngine</tt>).
*
* @param audioLevels a array mapping CSRC IDs to audio levels in
* consecutive elements.
*/
public void fireConferenceAudioLevelEvent(long[] audioLevels)
{
CsrcAudioLevelListener csrcAudioLevelListener
= this.csrcAudioLevelListener;
if (csrcAudioLevelListener != null)
csrcAudioLevelListener.audioLevelsReceived(audioLevels);
}
/**
* Delivers the <tt>DTMF</tt> tones. This
* method is meant for use primarily by the transform engine handling
* incoming RTP packets (currently <tt>DtmfTransformEngine</tt>).
*
* @param tone the new tone
* @param end is end or start of tone.
*/
public void fireDTMFEvent(DTMFRtpTone tone, boolean end)
{
Iterator<DTMFListener> iter = dtmfListeners.iterator();
DTMFToneEvent ev = new DTMFToneEvent(this, tone);
while (iter.hasNext())
{
DTMFListener listener = iter.next();
if(end)
listener.dtmfToneReceptionEnded(ev);
else
listener.dtmfToneReceptionStarted(ev);
}
}
/**
* 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()
{
super.close();
if(dtmfTransfrmEngine != null)
{
dtmfTransfrmEngine = null;
}
}
/**
* The priority of the audio is 3, which is meant to be higher than
* other threads and higher than the video one.
* @return audio priority.
*/
@Override
protected int getPriority()
{
return 3;
}
}