/* * 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; } }