From 0a3b48d5f3ca13f4d7eefbcff7e3da97810ca7fd Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Sat, 21 Nov 2009 15:32:56 +0000 Subject: [PATCH] Fixes the audio mixing functionality to not break the media of the whole conference when a member leaves it. --- .../device/AudioMixerMediaDevice.java | 217 ++++++++++++++++-- .../neomedia/device/MediaDeviceSession.java | 16 +- 2 files changed, 209 insertions(+), 24 deletions(-) diff --git a/src/net/java/sip/communicator/impl/neomedia/device/AudioMixerMediaDevice.java b/src/net/java/sip/communicator/impl/neomedia/device/AudioMixerMediaDevice.java index 240a6420c..e39402aa9 100644 --- a/src/net/java/sip/communicator/impl/neomedia/device/AudioMixerMediaDevice.java +++ b/src/net/java/sip/communicator/impl/neomedia/device/AudioMixerMediaDevice.java @@ -39,6 +39,12 @@ public class AudioMixerMediaDevice */ private final MediaDeviceImpl device; + /** + * The MediaDeviceSession of this AudioMixer with + * {@link #device}. + */ + private AudioMixerMediaDeviceSession deviceSession; + /** * Initializes a new AudioMixerMediaDevice instance which is to * enable audio mixing on a specific MediaDeviceImpl. @@ -83,9 +89,11 @@ AudioMixingPushBufferDataSource createOutputDataSource() * @see AbstractMediaDevice#createSession() */ @Override - public MediaDeviceSession createSession() + public synchronized MediaDeviceSession createSession() { - return new AudioMixerMediaDeviceSession(); + if (deviceSession == null) + deviceSession = new AudioMixerMediaDeviceSession(); + return new MediaStreamMediaDeviceSession(deviceSession); } /** @@ -155,32 +163,59 @@ public List getSupportedFormats() } /** - * Represents the MediaDeviceSession specific to one of the many - * possible MediaStreams using this MediaDevice for audio - * mixing. + * Represents the one and only MediaDeviceSession with the + * MediaDevice of this AudioMixer */ private class AudioMixerMediaDeviceSession extends MediaDeviceSession { + /** + * The list of MediaDeviceSessions of MediaStreams + * which use this AudioMixer. + */ + private final List + mediaStreamMediaDeviceSessions + = new LinkedList(); + /** * Initializes a new AudioMixingMediaDeviceSession which is to - * represent the MediaDeviceSession of one of the many possible - * MediaStreams using this MediaDevice for audio - * mixing. + * represent the MediaDeviceSession of this AudioMixer + * with its MediaDevice */ public AudioMixerMediaDeviceSession() { super(AudioMixerMediaDevice.this); } + /** + * Adds a specific MediaStreamMediaDeviceSession to the mix + * represented by this instance so that it knows when it is in use. + * + * @param mediaStreamMediaDeviceSession the + * MediaStreamMediaDeviceSession to be added to the mix + * represented by this instance + */ + void addMediaStreamMediaDeviceSession( + MediaStreamMediaDeviceSession mediaStreamMediaDeviceSession) + { + if (mediaStreamMediaDeviceSession == null) + throw new NullPointerException("mediaStreamMediaDeviceSession"); + + synchronized (mediaStreamMediaDeviceSessions) + { + if (!mediaStreamMediaDeviceSessions + .contains(mediaStreamMediaDeviceSession)) + mediaStreamMediaDeviceSessions + .add(mediaStreamMediaDeviceSession); + } + } + /** * Adds a ReceiveStream to this MediaDeviceSession to * be played back on the associated MediaDevice and a specific * DataSource is to be used to access its media data during the - * playback. The DataSource is explicitly specified in order to - * allow extenders to override the DataSource of the - * ReceiveStream (e.g. create a clone of it). + * playback. * * @param receiveStream the ReceiveStream to be played back by * this MediaDeviceSession on its associated @@ -196,30 +231,168 @@ protected void addReceiveStream( DataSource receiveStreamDataSource) { DataSource captureDevice = getCaptureDevice(); - AudioMixingPushBufferDataSource audioMixingDataSource; DataSource receiveStreamDataSourceForPlayback; if (captureDevice instanceof AudioMixingPushBufferDataSource) - { - audioMixingDataSource - = (AudioMixingPushBufferDataSource) captureDevice; receiveStreamDataSourceForPlayback - = getAudioMixer().getLocalOutputDataSource(); - } + = (AudioMixingPushBufferDataSource) captureDevice; else - { - audioMixingDataSource = null; receiveStreamDataSourceForPlayback = receiveStreamDataSource; - } super .addReceiveStream( receiveStream, receiveStreamDataSourceForPlayback); + } + + /** + * Creates the DataSource that this instance is to read + * captured media from. Since this is the MediaDeviceSession of + * this AudioMixer with its MediaDevice, returns the + * localOutputDataSource of the AudioMixer i.e. the + * DataSource which represents the mix of all + * ReceiveStreams and excludes the captured data from the + * MediaDevice of the AudioMixer. + * + * @return the DataSource that this instance is to read + * captured media from + * @see MediaDeviceSession#createCaptureDevice() + */ + @Override + protected DataSource createCaptureDevice() + { + return getAudioMixer().getLocalOutputDataSource(); + } + + /** + * Removes a specific MediaStreamMediaDeviceSession from the + * mix represented by this instance. When the last + * MediaStreamMediaDeviceSession is removed from this instance, + * it is no longer in use and closes itself thus signaling to its + * MediaDevice that it is no longer in use. + * + * @param mediaStreamMediaDeviceSession the + * MediaStreamMediaDeviceSession to be removed from the mix + * represented by this instance + */ + void removeMediaStreamMediaDeviceSession( + MediaStreamMediaDeviceSession mediaStreamMediaDeviceSession) + { + if (mediaStreamMediaDeviceSession != null) + synchronized (mediaStreamMediaDeviceSessions) + { + if (mediaStreamMediaDeviceSessions + .remove(mediaStreamMediaDeviceSession) + && mediaStreamMediaDeviceSessions.isEmpty()) + close(); + } + } + } + + /** + * Represents the work of a MediaStream with the + * MediaDevice of an AudioMixer and the contribution of + * that MediaStream to the mix. + */ + private static class MediaStreamMediaDeviceSession + extends MediaDeviceSession + { + + /** + * The MediaDeviceSession of the AudioMixer that this + * instance exposes to a MediaStream. While there are multiple + * MediaStreamMediaDeviceSessions each servicing a specific + * MediaStream, they all share and delegate to one and the same + * AudioMixerMediaDeviceSession so that they all contribute to + * the mix. + */ + private final AudioMixerMediaDeviceSession audioMixerMediaDeviceSession; + + /** + * Initializes a new MediaStreamMediaDeviceSession which is to + * represent the work of a MediaStream with the + * MediaDevice of this AudioMixer and its contribution + * to the mix. + * + * @param audioMixerMediaDeviceSession the MediaDeviceSession + * of the AudioMixer with its MediaDevice which the + * new instance is to delegate to in order to contribute to the mix + */ + public MediaStreamMediaDeviceSession( + AudioMixerMediaDeviceSession audioMixerMediaDeviceSession) + { + super(audioMixerMediaDeviceSession.getDevice()); + + this.audioMixerMediaDeviceSession = audioMixerMediaDeviceSession; + this.audioMixerMediaDeviceSession + .addMediaStreamMediaDeviceSession(this); + } + + /** + * Adds a ReceiveStream to this MediaDeviceSession to + * be played back on the associated MediaDevice and a specific + * DataSource is to be used to access its media data during the + * playback. + * + * @param receiveStream the ReceiveStream to be played back by + * this MediaDeviceSession on its associated + * MediaDevice + * @param receiveStreamDataSource the DataSource to be used for + * accessing the media data of receiveStream during its + * playback + * @see MediaDeviceSession#addReceiveStream(ReceiveStream, DataSource) + */ + @Override + protected void addReceiveStream( + ReceiveStream receiveStream, + DataSource receiveStreamDataSource) + { + audioMixerMediaDeviceSession + .addReceiveStream(receiveStream, receiveStreamDataSource); + + DataSource captureDevice = getCaptureDevice(); - if (audioMixingDataSource != null) - audioMixingDataSource + if (captureDevice instanceof AudioMixingPushBufferDataSource) + ((AudioMixingPushBufferDataSource) captureDevice) .addInputDataSource(receiveStreamDataSource); } + + /** + * Releases the resources allocated by this instance in the course of + * its execution and prepares it to be garbage collected. + * + * @see MediaDeviceSession#close() + */ + @Override + public void close() + { + try + { + super.close(); + } + finally + { + audioMixerMediaDeviceSession + .removeMediaStreamMediaDeviceSession(this); + } + } + + /** + * Removes a ReceiveStream from this + * MediaDeviceSession so that it no longer plays back on the + * associated MediaDevice. Since this is the + * MediaDeviceSession of a MediaStream, removes the + * specified ReceiveStream from the mix. + * + * @param receiveStream the ReceiveStream to be removed from + * this MediaDeviceSession and playback on the associated + * MediaDevice + * @see MediaDeviceSession#removeReceiveStream(ReceiveStream) + */ + @Override + public void removeReceiveStream(ReceiveStream receiveStream) + { + audioMixerMediaDeviceSession.removeReceiveStream(receiveStream); + } } } diff --git a/src/net/java/sip/communicator/impl/neomedia/device/MediaDeviceSession.java b/src/net/java/sip/communicator/impl/neomedia/device/MediaDeviceSession.java index 30cf6b46c..5715860fa 100644 --- a/src/net/java/sip/communicator/impl/neomedia/device/MediaDeviceSession.java +++ b/src/net/java/sip/communicator/impl/neomedia/device/MediaDeviceSession.java @@ -253,7 +253,7 @@ protected void addReceiveStream( // { // logger.error("The processor does not support effects", ex); // } - // to use the processor as player we must se its + // to use the processor as player we must set its // content descriptor to null player.setContentDescriptor(null); @@ -398,6 +398,18 @@ private void closeProcessor() } } + /** + * Creates the DataSource that this instance is to read captured + * media from. + * + * @return the DataSource that this instance is to read captured + * media from + */ + protected DataSource createCaptureDevice() + { + return getDevice().createOutputDataSource(); + } + /** * Makes sure {@link #captureDevice} is disconnected. */ @@ -514,7 +526,7 @@ private static Format findFirstMatchingFormat( protected DataSource getCaptureDevice() { if (captureDevice == null) - captureDevice = getDevice().createOutputDataSource(); + captureDevice = createCaptureDevice(); return captureDevice; }