
org.jitsi.impl.neomedia.conference.AudioMixer 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.conference;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import javax.media.*;
import javax.media.Controls;
import javax.media.control.*;
import javax.media.format.*;
import javax.media.protocol.*;
import org.jitsi.impl.neomedia.*;
import org.jitsi.impl.neomedia.control.*;
import org.jitsi.impl.neomedia.device.*;
import org.jitsi.impl.neomedia.protocol.*;
import org.jitsi.util.*;
import org.jitsi.utils.logging.*;
/**
* Represents an audio mixer which manages the mixing of multiple audio streams
* i.e. it is able to output a single audio stream which contains the audio of
* multiple input audio streams.
*
* The input audio streams are provided to the AudioMixer through
* {@link #addInDataSource(DataSource)} in the form of input
* DataSources giving access to one or more input
* SourceStreams.
*
* The output audio stream representing the mix of the multiple input audio
* streams is provided by the AudioMixer in the form of a
* AudioMixingPushBufferDataSource giving access to a
* AudioMixingPushBufferStream. Such an output is obtained through
* {@link #createOutDataSource()}. The AudioMixer is able to provide
* multiple output audio streams at one and the same time, though, each of them
* containing the mix of a subset of the input audio streams.
*
* @author Lyubomir Marinov
*/
public class AudioMixer
{
/**
* The default output AudioFormat in which AudioMixer,
* AudioMixingPushBufferDataSource and
* AudioMixingPushBufferStream output audio.
*/
private static final AudioFormat DEFAULT_OUTPUT_FORMAT
= new AudioFormat(
AudioFormat.LINEAR,
8000,
16,
1,
AudioFormat.LITTLE_ENDIAN,
AudioFormat.SIGNED);
/**
* The Logger used by the AudioMixer class and its
* instances for logging output.
*/
private static final Logger logger = Logger.getLogger(AudioMixer.class);
/**
* Gets the Format in which a specific DataSource
* provides stream data.
*
* @param dataSource the DataSource for which the Format
* in which it provides stream data is to be determined
* @return the Format in which the specified dataSource
* provides stream data if it was determined; otherwise, null
*/
private static Format getFormat(DataSource dataSource)
{
FormatControl formatControl
= (FormatControl)
dataSource.getControl(FormatControl.class.getName());
return (formatControl == null) ? null : formatControl.getFormat();
}
/**
* Gets the Format in which a specific SourceStream
* provides data.
*
* @param stream the SourceStream for which the Format in
* which it provides data is to be determined
* @return the Format in which the specified SourceStream
* provides data if it was determined; otherwise, null
*/
private static Format getFormat(SourceStream stream)
{
if (stream instanceof PushBufferStream)
return ((PushBufferStream) stream).getFormat();
if (stream instanceof PullBufferStream)
return ((PullBufferStream) stream).getFormat();
return null;
}
/**
* The BufferControl of this instance and, respectively, its
* AudioMixingPushBufferDataSources.
*/
private BufferControl bufferControl;
/**
* The CaptureDevice capabilities provided by the
* AudioMixingPushBufferDataSources created by this
* AudioMixer. JMF's
* Manager.createMergingDataSource(DataSource[]) requires the
* interface implementation for audio if it is implemented for video and it
* is indeed the case for our use case of
* AudioMixingPushBufferDataSource.
*/
protected final CaptureDevice captureDevice;
/**
* The number of output AudioMixingPushBufferDataSources reading
* from this AudioMixer which are connected. When the value is
* greater than zero, this AudioMixer is connected to the input
* DataSources it manages.
*/
private int connected;
/**
* The collection of input DataSources this instance reads audio
* data from.
*/
private final List inDataSources
= new ArrayList();
/**
* The AudioMixingPushBufferDataSource which contains the mix of
* inDataSources excluding captureDevice and is thus
* meant for playback on the local peer in a call.
*/
private final AudioMixingPushBufferDataSource localOutDataSource;
/**
* The output AudioMixerPushBufferStream through which this
* instance pushes audio sample data to
* AudioMixingPushBufferStreams to be mixed.
*/
private AudioMixerPushBufferStream outStream;
/**
* The number of output AudioMixingPushBufferDataSources reading
* from this AudioMixer which are started. When the value is
* greater than zero, this AudioMixer is started and so are the
* input DataSources it manages.
*/
private int started;
/**
* The greatest generation with which
* {@link #start(AudioMixerPushBufferStream, long)} or
* {@link #stop(AudioMixerPushBufferStream, long)} has been invoked.
*/
private long startedGeneration;
/**
* Initializes a new AudioMixer instance. Because JMF's
* Manager.createMergingDataSource(DataSource[]) requires the
* implementation of CaptureDevice for audio if it is implemented
* for video and it is indeed the cause for our use case of
* AudioMixingPushBufferDataSource, the new AudioMixer
* instance provides specified CaptureDevice capabilities to the
* AudioMixingPushBufferDataSources it creates. The specified
* CaptureDevice is also added as the first input
* DataSource of the new instance.
*
* @param captureDevice the CaptureDevice capabilities to be
* provided to the AudioMixingPushBufferDataSources created by the
* new instance and its first input DataSource
*/
public AudioMixer(CaptureDevice captureDevice)
{
/*
* AudioMixer provides PushBufferDataSources so it needs a way to push
* them. It does the pushing by using the pushes of its CaptureDevice
* i.e. it has to be a PushBufferDataSource.
*/
if (captureDevice instanceof PullBufferDataSource)
{
captureDevice
= new PushBufferDataSourceAdapter(
(PullBufferDataSource) captureDevice);
}
// Try to enable tracing on captureDevice.
if (logger.isTraceEnabled())
{
captureDevice
= MediaDeviceImpl.createTracingCaptureDevice(
captureDevice,
logger);
}
this.captureDevice = captureDevice;
this.localOutDataSource = createOutDataSource();
addInDataSource(
(DataSource) this.captureDevice,
this.localOutDataSource);
}
/**
* Adds a new input DataSource to the collection of input
* DataSources from which this instance reads audio. If the
* specified DataSource indeed provides audio, the respective
* contributions to the mix are always included.
*
* @param inDataSource a new DataSource to input audio to this
* instance
*/
public void addInDataSource(DataSource inDataSource)
{
addInDataSource(inDataSource, null);
}
/**
* Adds a new input DataSource to the collection of input
* DataSources from which this instance reads audio. If the
* specified DataSource indeed provides audio, the respective
* contributions to the mix will be excluded from the mix output provided
* through a specific AudioMixingPushBufferDataSource.
*
* @param inDataSource a new DataSource to input audio to this
* instance
* @param outDataSource the AudioMixingPushBufferDataSource to
* not include the audio contributions of inDataSource in the
* mix it outputs
*/
void addInDataSource(
DataSource inDataSource,
AudioMixingPushBufferDataSource outDataSource)
{
if (inDataSource == null)
throw new NullPointerException("inDataSource");
synchronized (inDataSources)
{
for (InDataSourceDesc inDataSourceDesc : inDataSources)
if (inDataSource.equals(inDataSourceDesc.inDataSource))
throw new IllegalArgumentException("inDataSource");
InDataSourceDesc inDataSourceDesc
= new InDataSourceDesc(
inDataSource,
outDataSource);
boolean added = inDataSources.add(inDataSourceDesc);
if (added)
{
if (logger.isTraceEnabled())
{
logger.trace(
"Added input DataSource with hashCode "
+ inDataSource.hashCode());
}
/*
* If the other inDataSources have already been connected,
* connect to the new one as well.
*/
if (connected > 0)
{
try
{
inDataSourceDesc.connect(this);
}
catch (IOException ioex)
{
throw new UndeclaredThrowableException(ioex);
}
}
// Update outStream with any new inStreams.
if (outStream != null)
getOutStream();
/*
* If the other inDataSources have been started, start the
* new one as well.
*/
if (started > 0)
{
try
{
inDataSourceDesc.start();
}
catch (IOException ioe)
{
throw new UndeclaredThrowableException(ioe);
}
}
}
}
}
/**
* Notifies this AudioMixer that an output
* AudioMixingPushBufferDataSource reading from it has been
* connected. The first of the many
* AudioMixingPushBufferDataSources reading from this
* AudioMixer which gets connected causes it to connect to the
* input DataSources it manages.
*
* @throws IOException if input/output error occurred
*/
void connect()
throws IOException
{
synchronized (inDataSources)
{
if (connected == 0)
{
for (InDataSourceDesc inDataSourceDesc : inDataSources)
try
{
inDataSourceDesc.connect(this);
}
catch (IOException ioe)
{
logger.error(
"Failed to connect to inDataSource "
+ MediaStreamImpl.toString(
inDataSourceDesc.inDataSource),
ioe);
throw ioe;
}
/*
* Since the media of the input streams is to be mixed, their
* bufferLengths have to be equal. After a DataSource is
* connected, its BufferControl is available and its
* bufferLength may change so make sure that the bufferLengths
* of the input streams are equal.
*/
if (outStream != null)
outStream.equalizeInStreamBufferLength();
}
connected++;
}
}
/**
* Connects to a specific DataSource which this AudioMixer
* will read audio from. The specified DataSource is known to exist
* because of a specific DataSource added as an input to this
* instance i.e. it may be an actual input DataSource added to this
* instance or a DataSource transcoding an input
* DataSource added to this instance.
*
* @param dataSource the DataSource to connect to
* @param inDataSource the DataSource which is the cause for
* dataSource to exist in this AudioMixer
* @throws IOException if anything wrong happens while connecting to
* dataSource
*/
protected void connect(DataSource dataSource, DataSource inDataSource)
throws IOException
{
dataSource.connect();
}
/**
* Notifies this AudioMixer that a specific input
* DataSource has finished its connecting procedure. Primarily
* meant for input DataSource which have their connecting executed
* in a separate thread as are, for example, input DataSources
* which are being transcoded.
*
* @param inDataSource the InDataSourceDesc of the input
* DataSource which has finished its connecting procedure
* @throws IOException if anything wrong happens while including
* inDataSource into the mix
*/
void connected(InDataSourceDesc inDataSource)
throws IOException
{
synchronized (inDataSources)
{
if (inDataSources.contains(inDataSource)
&& (connected > 0))
{
if (started > 0)
inDataSource.start();
if (outStream != null)
getOutStream();
}
}
}
/**
* Creates a new InStreamDesc instance which is to describe a
* specific input SourceStream originating from a specific input
* DataSource given by its InDataSourceDesc.
*
* @param inStream the input SourceStream to be described by the
* new instance
* @param inDataSourceDesc the input DataSource given by its
* InDataSourceDesc to be described by the new instance
* @return a new InStreamDesc instance which describes the
* specified input SourceStream and DataSource
*/
private InStreamDesc createInStreamDesc(
SourceStream inStream,
InDataSourceDesc inDataSourceDesc)
{
return new InStreamDesc(inStream, inDataSourceDesc);
}
/**
* Creates a new AudioMixingPushBufferDataSource which gives
* access to a single audio stream representing the mix of the audio streams
* input into this AudioMixer through its input
* DataSources. The returned
* AudioMixingPushBufferDataSource can also be used to include
* new input DataSources in this AudioMixer but
* have their contributions not included in the mix available through the
* returned AudioMixingPushBufferDataSource.
*
* @return a new AudioMixingPushBufferDataSource which gives access
* to a single audio stream representing the mix of the audio streams input
* into this AudioMixer through its input DataSources
*/
public AudioMixingPushBufferDataSource createOutDataSource()
{
return new AudioMixingPushBufferDataSource(this);
}
/**
* Creates a DataSource which attempts to transcode the tracks of a
* specific input DataSource into a specific output
* Format.
*
* @param inDataSourceDesc the InDataSourceDesc describing
* the input DataSource to be transcoded into the specified output
* Format and to receive the transcoding DataSource
* @param outFormat the Format in which the tracks of the input
* DataSource are to be transcoded
* @return true if a new transcoding DataSource has been
* created for the input DataSource described by
* inDataSourceDesc; otherwise, false
* @throws IOException if an error occurs while creating the transcoding
* DataSource, connecting to it or staring it
*/
private boolean createTranscodingDataSource(
InDataSourceDesc inDataSourceDesc,
Format outFormat)
throws IOException
{
if (inDataSourceDesc.createTranscodingDataSource(outFormat))
{
if (connected > 0)
inDataSourceDesc.connect(this);
if (started > 0)
inDataSourceDesc.start();
return true;
}
else
return false;
}
/**
* Notifies this AudioMixer that an output
* AudioMixingPushBufferDataSource reading from it has been
* disconnected. The last of the many
* AudioMixingPushBufferDataSources reading from this
* AudioMixer which gets disconnected causes it to disconnect
* from the input DataSources it manages.
*/
void disconnect()
{
synchronized (inDataSources)
{
if (connected <= 0)
return;
connected--;
if (connected == 0)
{
for (InDataSourceDesc inDataSourceDesc : inDataSources)
inDataSourceDesc.disconnect();
/*
* XXX Make the outStream to release the inStreams.
* Otherwise, the PushBufferStream ones which have been wrapped
* into CachingPushBufferStream may remain waiting.
*/
outStream.setInStreams(null);
outStream = null;
startedGeneration = 0;
}
}
}
/**
* Gets the BufferControl of this instance and, respectively, its
* AudioMixingPushBufferDataSources.
*
* @return the BufferControl of this instance and, respectively,
* its AudioMixingPushBufferDataSources if such a control is
* available for the CaptureDevice of this instance; otherwise,
* null
*/
BufferControl getBufferControl()
{
if ((bufferControl == null) && (captureDevice instanceof Controls))
{
BufferControl captureDeviceBufferControl
= (BufferControl)
((Controls) captureDevice).getControl(
BufferControl.class.getName());
if (captureDeviceBufferControl != null)
bufferControl
= new ReadOnlyBufferControlDelegate(
captureDeviceBufferControl);
}
return bufferControl;
}
/**
* Gets the CaptureDeviceInfo of the CaptureDevice
* this AudioMixer provides through its output
* AudioMixingPushBufferDataSources.
*
* @return the CaptureDeviceInfo of the CaptureDevice this
* AudioMixer provides through its output
* AudioMixingPushBufferDataSources
*/
CaptureDeviceInfo getCaptureDeviceInfo()
{
return captureDevice.getCaptureDeviceInfo();
}
/**
* Gets the content type of the data output by this AudioMixer.
*
* @return the content type of the data output by this AudioMixer
*/
String getContentType()
{
return ContentDescriptor.RAW;
}
/**
* Gets the duration of each one of the output streams produced by this
* AudioMixer.
*
* @return the duration of each one of the output streams produced by this
* AudioMixer
*/
Time getDuration()
{
return ((DataSource) captureDevice).getDuration();
}
/**
* Gets an InStreamDesc from a specific existing list of
* InStreamDescs which describes a specific
* SourceStream. If such an InStreamDesc does not
* exist, returns null.
*
* @param inStream the SourceStream to locate an
* InStreamDesc for in existingInStreamDescs
* @param existingInStreamDescs the list of existing
* InStreamDescs in which an InStreamDesc for
* inStream is to be located
* @return an InStreamDesc from
* existingInStreamDescs which describes inStream if
* such an InStreamDesc exists; otherwise, null
*/
private InStreamDesc getExistingInStreamDesc(
SourceStream inStream,
InStreamDesc[] existingInStreamDescs)
{
if (existingInStreamDescs == null)
return null;
for (InStreamDesc existingInStreamDesc
: existingInStreamDescs)
{
SourceStream existingInStream
= existingInStreamDesc.getInStream();
if (existingInStream == inStream)
return existingInStreamDesc;
if ((existingInStream instanceof BufferStreamAdapter>)
&& (((BufferStreamAdapter>) existingInStream).getStream()
== inStream))
return existingInStreamDesc;
if ((existingInStream instanceof CachingPushBufferStream)
&& (((CachingPushBufferStream) existingInStream).getStream()
== inStream))
return existingInStreamDesc;
}
return null;
}
/**
* Gets an array of FormatControls for the
* CaptureDevice this AudioMixer provides through
* its output AudioMixingPushBufferDataSources.
*
* @return an array of FormatControls for the
* CaptureDevice this AudioMixer provides
* through its output AudioMixingPushBufferDataSources
*/
FormatControl[] getFormatControls()
{
/*
* Setting the format of the captureDevice once we've started using it
* is likely to wreak havoc so disable it.
*/
FormatControl[] formatControls = captureDevice.getFormatControls();
if (!OSUtils.IS_ANDROID && (formatControls != null))
{
for (int i = 0; i < formatControls.length; i++)
{
formatControls[i]
= new ReadOnlyFormatControlDelegate(formatControls[i]);
}
}
return formatControls;
}
/**
* Gets the SourceStreams (in the form of InStreamDesc)
* of a specific DataSource (provided in the form of
* InDataSourceDesc) which produce data in a specific
* AudioFormat (or a matching one).
*
* @param inDataSourceDesc the DataSource (in the form of
* InDataSourceDesc) which is to be examined for
* SourceStreams producing data in the specified
* AudioFormat
* @param outFormat the AudioFormat in which the collected
* SourceStreams are to produce data
* @param existingInStreams the InStreamDesc instances which
* already exist and which are used to avoid creating multiple
* InStreamDescs for input SourceStreams which already
* have ones
* @param inStreams the List of InStreamDesc in
* which the discovered SourceStreams are to be returned
* @return true if SourceStreams produced by the specified
* input DataSource and outputting data in the specified
* AudioFormat were discovered and reported in
* inStreams; otherwise, false
*/
private boolean getInStreamsFromInDataSource(
InDataSourceDesc inDataSourceDesc,
AudioFormat outFormat,
InStreamDesc[] existingInStreams,
List inStreams)
{
SourceStream[] inDataSourceStreams = inDataSourceDesc.getStreams();
if (inDataSourceStreams != null)
{
boolean added = false;
for (SourceStream inStream : inDataSourceStreams)
{
Format inFormat = getFormat(inStream);
if ((inFormat != null) && matches(inFormat, outFormat))
{
InStreamDesc inStreamDesc
= getExistingInStreamDesc(inStream, existingInStreams);
if (inStreamDesc == null)
inStreamDesc
= createInStreamDesc(inStream, inDataSourceDesc);
if (inStreams.add(inStreamDesc))
added = true;
}
}
return added;
}
DataSource inDataSource = inDataSourceDesc.getEffectiveInDataSource();
if (inDataSource == null)
return false;
Format inFormat = getFormat(inDataSource);
if ((inFormat != null) && !matches(inFormat, outFormat))
{
if (inDataSource instanceof PushDataSource)
{
for (PushSourceStream inStream
: ((PushDataSource) inDataSource).getStreams())
{
InStreamDesc inStreamDesc
= getExistingInStreamDesc(inStream, existingInStreams);
if (inStreamDesc == null)
inStreamDesc
= createInStreamDesc(
new PushBufferStreamAdapter(
inStream,
inFormat),
inDataSourceDesc);
inStreams.add(inStreamDesc);
}
return true;
}
if (inDataSource instanceof PullDataSource)
{
for (PullSourceStream inStream
: ((PullDataSource) inDataSource).getStreams())
{
InStreamDesc inStreamDesc
= getExistingInStreamDesc(inStream, existingInStreams);
if (inStreamDesc == null)
inStreamDesc
= createInStreamDesc(
new PullBufferStreamAdapter(
inStream,
inFormat),
inDataSourceDesc);
inStreams.add(inStreamDesc);
}
return true;
}
}
return false;
}
/**
* Gets the SourceStreams (in the form of InStreamDesc)
* of the DataSources from which this AudioMixer reads
* data which produce data in a specific AudioFormat. When an input
* DataSource does not have such SourceStreams, an attempt
* is made to transcode its tracks so that such SourceStreams can
* be retrieved from it after transcoding.
*
* @param outFormat the AudioFormat in which the retrieved
* SourceStreams are to produce data
* @param existingInStreams the SourceStreams which are already
* known to this AudioMixer
* @return a new collection of SourceStreams (in the form of
* InStreamDesc) retrieved from the input DataSources
* of this AudioMixer and producing data in the specified
* AudioFormat
* @throws IOException if anything wrong goes while retrieving the input
* SourceStreams from the input DataSources
*/
private Collection getInStreamsFromInDataSources(
AudioFormat outFormat,
InStreamDesc[] existingInStreams)
throws IOException
{
List inStreams = new ArrayList();
synchronized (inDataSources)
{
for (InDataSourceDesc inDataSourceDesc : inDataSources)
{
boolean got
= getInStreamsFromInDataSource(
inDataSourceDesc,
outFormat,
existingInStreams,
inStreams);
if (!got
&& createTranscodingDataSource(
inDataSourceDesc,
outFormat))
getInStreamsFromInDataSource(
inDataSourceDesc,
outFormat,
existingInStreams,
inStreams);
}
}
return inStreams;
}
/**
* Gets the AudioMixingPushBufferDataSource containing the mix of
* all input DataSources excluding the CaptureDevice of
* this AudioMixer and is thus meant for playback on the local peer
* in a call.
*
* @return the AudioMixingPushBufferDataSource containing the mix
* of all input DataSources excluding the CaptureDevice of
* this AudioMixer and is thus meant for playback on the local peer
* in a call
*/
public AudioMixingPushBufferDataSource getLocalOutDataSource()
{
return localOutDataSource;
}
/**
* Gets the AudioFormat in which the input
* DataSources of this AudioMixer can produce data
* and which is to be the output Format of this
* AudioMixer.
*
* @return the AudioFormat in which the input
* DataSources of this AudioMixer can
* produce data and which is to be the output Format of
* this AudioMixer
*/
private AudioFormat getOutFormatFromInDataSources()
{
String formatControlType = FormatControl.class.getName();
AudioFormat outFormat = null;
synchronized (inDataSources)
{
for (InDataSourceDesc inDataSource : inDataSources)
{
DataSource effectiveInDataSource
= inDataSource.getEffectiveInDataSource();
if (effectiveInDataSource == null)
continue;
FormatControl formatControl
= (FormatControl)
effectiveInDataSource.getControl(formatControlType);
if (formatControl != null)
{
AudioFormat format
= (AudioFormat) formatControl.getFormat();
if (format != null)
{
// SIGNED
int signed = format.getSigned();
if ((AudioFormat.SIGNED == signed)
|| (Format.NOT_SPECIFIED == signed))
{
// LITTLE_ENDIAN
int endian = format.getEndian();
if ((AudioFormat.LITTLE_ENDIAN == endian)
|| (Format.NOT_SPECIFIED == endian))
{
outFormat = format;
break;
}
}
}
}
}
}
if (outFormat == null)
outFormat = DEFAULT_OUTPUT_FORMAT;
if (logger.isTraceEnabled())
{
logger.trace(
"Determined outFormat of AudioMixer from inDataSources" +
" to be " + outFormat);
}
return outFormat;
}
/**
* Gets the AudioMixerPushBufferStream, first creating it if it
* does not exist already, which reads data from the input
* DataSources of this AudioMixer and pushes it to
* output AudioMixingPushBufferStreams for audio mixing.
*
* @return the AudioMixerPushBufferStream which reads data from
* the input DataSources of this AudioMixer and pushes it
* to output AudioMixingPushBufferStreams for audio mixing
*/
AudioMixerPushBufferStream getOutStream()
{
synchronized (inDataSources)
{
AudioFormat outFormat
= (outStream == null)
? getOutFormatFromInDataSources()
: outStream.getFormat();
setOutFormatToInDataSources(outFormat);
Collection inStreams;
try
{
inStreams
= getInStreamsFromInDataSources(
outFormat,
(outStream == null) ? null : outStream.getInStreams());
}
catch (IOException ioex)
{
throw new UndeclaredThrowableException(ioex);
}
if (outStream == null)
{
outStream = new AudioMixerPushBufferStream(this, outFormat);
startedGeneration = 0;
}
outStream.setInStreams(inStreams);
return outStream;
}
}
/**
* Searches this object's inDataSources for one that matches
* inDataSource, and returns it's associated
* TranscodingDataSource. Currently this is only used when
* the MediaStream needs access to the codec chain used to
* playback one of it's ReceiveStreams.
*
* @param inDataSource the DataSource to search for.
* @return The TranscodingDataSource associated with
* inDataSource, if we can find one, null otherwise.
*/
public TranscodingDataSource getTranscodingDataSource(
DataSource inDataSource)
{
for (InDataSourceDesc inDataSourceDesc : inDataSources)
{
DataSource ourDataSource = inDataSourceDesc.getInDataSource();
if (ourDataSource == inDataSource)
return inDataSourceDesc.getTranscodingDataSource();
else if (ourDataSource instanceof ReceiveStreamPushBufferDataSource)
{
// Sometimes the inDataSource has come to AudioMixer wrapped in
// a ReceiveStreamPushBufferDataSource. We consider it to match.
if (((ReceiveStreamPushBufferDataSource) ourDataSource)
.getDataSource()
== inDataSource)
return inDataSourceDesc.getTranscodingDataSource();
}
}
return null;
}
/**
* Determines whether a specific Format matches a specific
* Format in the sense of JMF Format matching.
* Since this AudioMixer and the audio mixing functionality
* related to it can handle varying characteristics of a certain output
* Format, the only requirement for the specified
* Formats to match is for both of them to have one and the
* same encoding.
*
* @param input the Format for which it is required to determine
* whether it matches a specific Format
* @param pattern the Format against which the specified
* input is to be matched
* @return true if the specified input matches the
* specified pattern in the sense of JMF Format matching;
* otherwise, false
*/
private boolean matches(Format input, AudioFormat pattern)
{
return
((input instanceof AudioFormat) && input.isSameEncoding(pattern));
}
/**
* Reads media from a specific PushBufferStream which belongs to
* a specific DataSource into a specific output Buffer.
* Allows extenders to tap into the reading and monitor and customize it.
*
* @param stream the PushBufferStream to read media from and known
* to belong to the specified DataSOurce
* @param buffer the output Buffer in which the media read from the
* specified stream is to be written so that it gets returned to
* the caller
* @param dataSource the DataSource from which stream
* originated
* @throws IOException if anything wrong happens while reading from the
* specified stream
*/
protected void read(
PushBufferStream stream,
Buffer buffer,
DataSource dataSource)
throws IOException
{
stream.read(buffer);
}
/**
* Removes DataSources accepted by a specific
* DataSourceFilter from the list of input DataSources of
* this AudioMixer from which it reads audio to be mixed.
*
* @param dataSourceFilter the DataSourceFilter which selects the
* DataSources to be removed from the list of input
* DataSources of this AudioMixer from which it reads
* audio to be mixed
*/
public void removeInDataSources(DataSourceFilter dataSourceFilter)
{
synchronized (inDataSources)
{
Iterator inDataSourceIter
= inDataSources.iterator();
boolean removed = false;
while (inDataSourceIter.hasNext())
{
InDataSourceDesc inDsDesc = inDataSourceIter.next();
if (dataSourceFilter.accept(inDsDesc.getInDataSource()))
{
inDataSourceIter.remove();
removed = true;
try
{
inDsDesc.stop();
inDsDesc.disconnect();
}
catch(IOException ex)
{
logger.error("Failed to stop DataSource", ex);
}
}
}
if (removed && (outStream != null))
getOutStream();
}
}
/**
* Sets a specific AudioFormat, if possible, as the output
* format of the input DataSources of this
* AudioMixer in an attempt to not have to perform explicit
* transcoding of the input SourceStreams.
*
* @param outFormat the AudioFormat in which the input
* DataSources of this AudioMixer are to be instructed to
* output
*/
private void setOutFormatToInDataSources(AudioFormat outFormat)
{
String formatControlType = FormatControl.class.getName();
synchronized (inDataSources)
{
for (InDataSourceDesc inDataSourceDesc : inDataSources)
{
FormatControl formatControl
= (FormatControl)
inDataSourceDesc.getControl(formatControlType);
if (formatControl != null)
{
Format inFormat = formatControl.getFormat();
if ((inFormat == null) || !matches(inFormat, outFormat))
{
Format setFormat
= formatControl.setFormat(outFormat);
if (setFormat == null)
logger.error(
"Failed to set format of inDataSource to "
+ outFormat);
else if (setFormat != outFormat)
logger.warn(
"Failed to change format of inDataSource"
+ " from " + setFormat + " to "
+ outFormat);
else if (logger.isTraceEnabled())
logger.trace(
"Set format of inDataSource to "
+ setFormat);
}
}
}
}
}
/**
* Starts the input DataSources of this AudioMixer.
*
* @param outStream the AudioMixerPushBufferStream which requests
* this AudioMixer to start. If outStream is the current
* one and only AudioMixerPushBufferStream of this
* AudioMixer, this AudioMixer starts if it hasn't started
* yet. Otherwise, the request is ignored.
* @param generation a value generated by outStream indicating the
* order of the invocations of the start and stop methods
* performed by outStream allowing it to execute the said methods
* outside synchronized blocks for the purposes of reducing deadlock risks
* @throws IOException if any of the input DataSources of this
* AudioMixer throws such an exception while attempting to start it
*/
void start(AudioMixerPushBufferStream outStream, long generation)
throws IOException
{
synchronized (inDataSources)
{
/*
* AudioMixer has only one outStream at a time and only its current
* outStream knows when it has to start (and stop).
*/
if (this.outStream != outStream)
return;
/*
* The notion of generations was introduced in order to allow
* outStream to invoke the start and stop methods outside
* synchronized blocks. The generation value always increases in a
* synchronized block.
*/
if (startedGeneration < generation)
startedGeneration = generation;
else
return;
if (started == 0)
{
for (InDataSourceDesc inDataSourceDesc : inDataSources)
inDataSourceDesc.start();
}
started++;
}
}
/**
* Stops the input DataSources of this AudioMixer.
*
* @param outStream the AudioMixerPushBufferStream which requests
* this AudioMixer to stop. If outStream is the current
* one and only AudioMixerPushBufferStream of this
* AudioMixer, this AudioMixer stops. Otherwise, the
* request is ignored.
* @param generation a value generated by outStream indicating the
* order of the invocations of the start and stop methods
* performed by outStream allowing it to execute the said methods
* outside synchronized blocks for the purposes of reducing deadlock risks
* @throws IOException if any of the input DataSources of this
* AudioMixer throws such an exception while attempting to stop it
*/
void stop(AudioMixerPushBufferStream outStream, long generation)
throws IOException
{
synchronized (inDataSources)
{
/*
* AudioMixer has only one outStream at a time and only its current
* outStream knows when it has to stop (and start).
*/
if (this.outStream != outStream)
return;
/*
* The notion of generations was introduced in order to allow
* outStream to invoke the start and stop methods outside
* synchronized blocks. The generation value always increases in a
* synchronized block.
*/
if (startedGeneration < generation)
startedGeneration = generation;
else
return;
if (started <= 0)
return;
started--;
if (started == 0)
{
for (InDataSourceDesc inDataSourceDesc : inDataSources)
inDataSourceDesc.stop();
}
}
}
}