diff --git a/src/net/java/sip/communicator/impl/media/CaptureDeviceDelegatePushBufferDataSource.java b/src/net/java/sip/communicator/impl/media/CaptureDeviceDelegatePushBufferDataSource.java new file mode 100644 index 000000000..8917f80a1 --- /dev/null +++ b/src/net/java/sip/communicator/impl/media/CaptureDeviceDelegatePushBufferDataSource.java @@ -0,0 +1,70 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.media; + +import javax.media.*; +import javax.media.control.*; +import javax.media.protocol.*; + +/** + * Represents a PushBufferDataSource which is also a + * CaptureDevice through delegation to a specific + * CaptureDevice. + * + * @author Lubomir Marinov + */ +public abstract class CaptureDeviceDelegatePushBufferDataSource + extends PushBufferDataSource + implements CaptureDevice +{ + + /** + * The CaptureDevice this instance delegates to in order to + * implement its CaptureDevice functionality. + */ + private final CaptureDevice captureDevice; + + /** + * Initializes a new CaptureDeviceDelegatePushBufferDataSource + * instance which delegates to a specific CaptureDevice in + * order to implement its CaptureDevice functionality. + * + * @param captureDevice the CaptureDevice the new instance is + * to delegate to in order to provide its + * CaptureDevice functionality + */ + public CaptureDeviceDelegatePushBufferDataSource( + CaptureDevice captureDevice) + { + this.captureDevice = captureDevice; + } + + /* + * Implements CaptureDevice#getCaptureDeviceInfo(). Delegates to the wrapped + * CaptureDevice if available; otherwise, returns null. + */ + public CaptureDeviceInfo getCaptureDeviceInfo() + { + return + (captureDevice != null) + ? captureDevice.getCaptureDeviceInfo() + : null; + } + + /* + * Implements CaptureDevice#getFormatControls(). Delegates to the wrapped + * CaptureDevice if available; otherwise, returns an empty array of + * FormatControl. + */ + public FormatControl[] getFormatControls() + { + return + (captureDevice != null) + ? captureDevice.getFormatControls() + : new FormatControl[0]; + } +} diff --git a/src/net/java/sip/communicator/impl/media/MutePushBufferDataSource.java b/src/net/java/sip/communicator/impl/media/MutePushBufferDataSource.java index c4f3d2b8f..1e83bbc6f 100644 --- a/src/net/java/sip/communicator/impl/media/MutePushBufferDataSource.java +++ b/src/net/java/sip/communicator/impl/media/MutePushBufferDataSource.java @@ -8,8 +8,8 @@ import java.io.*; import java.util.*; + import javax.media.*; -import javax.media.control.*; import javax.media.protocol.*; /** @@ -24,8 +24,7 @@ * @author Lubomir Marinov */ public class MutePushBufferDataSource - extends PushBufferDataSource - implements CaptureDevice + extends CaptureDeviceDelegatePushBufferDataSource { /** @@ -47,62 +46,73 @@ public class MutePushBufferDataSource */ public MutePushBufferDataSource(PushBufferDataSource dataSource) { + super( + (dataSource instanceof CaptureDevice) + ? (CaptureDevice) dataSource + : null); + this.dataSource = dataSource; } + /* + * Implements DataSource#connect(). Delegates to the wrapped + * PushBufferDataSource. + */ public void connect() throws IOException { dataSource.connect(); } + /* + * Implements DataSource#disconnect(). Delegates to the wrapped + * PushBufferDataSource. + */ public void disconnect() { dataSource.disconnect(); } - public CaptureDeviceInfo getCaptureDeviceInfo() - { - CaptureDeviceInfo captureDeviceInfo; - - if (dataSource instanceof CaptureDevice) - captureDeviceInfo = - ((CaptureDevice) dataSource).getCaptureDeviceInfo(); - else - captureDeviceInfo = null; - return captureDeviceInfo; - } - + /* + * Implements DataSource#getContentType(). Delegates to the wrapped + * PushBufferDataSource. + */ public String getContentType() { return dataSource.getContentType(); } + /* + * Implements DataSource#getControl(String). Delegates to the wrapped + * PushBufferDataSource. + */ public Object getControl(String controlType) { return dataSource.getControl(controlType); } + /* + * Implements DataSource#getControls(). Delegates to the wrapped + * PushBufferDataSource. + */ public Object[] getControls() { return dataSource.getControls(); } + /* + * Implements DataSource#getDuration(). Delegates to the wrapped + * PushBufferDataSource. + */ public Time getDuration() { return dataSource.getDuration(); } - public FormatControl[] getFormatControls() - { - FormatControl[] formatControls; - - if (dataSource instanceof CaptureDevice) - formatControls = ((CaptureDevice) dataSource).getFormatControls(); - else - formatControls = new FormatControl[0]; - return formatControls; - } - + /* + * Implements PushBufferDataSource#getStreams(). Wraps the streams of the + * wrapped PushBufferDataSource into MutePushBufferStream instances in order + * to provide mute support to them. + */ public PushBufferStream[] getStreams() { PushBufferStream[] streams = dataSource.getStreams(); @@ -136,11 +146,19 @@ public synchronized void setMute(boolean mute) this.mute = mute; } + /* + * Implements DataSource#start(). Delegates to the wrapped + * PushBufferDataSource. + */ public void start() throws IOException { dataSource.start(); } + /* + * Implements DataSource#stop(). Delegates to the wrapped + * PushBufferDataSource. + */ public void stop() throws IOException { dataSource.stop(); @@ -171,36 +189,65 @@ public MutePushBufferStream(PushBufferStream stream) this.stream = stream; } + /* + * Implements SourceStream#getContentDescriptor(). Delegates to the + * wrapped PushBufferStream. + */ public ContentDescriptor getContentDescriptor() { return stream.getContentDescriptor(); } + /* + * Implements SourceStream#getContentLength(). Delegates to the wrapped + * PushBufferStream. + */ public long getContentLength() { return stream.getContentLength(); } + /* + * Implements Controls#getControl(String). Delegates to the wrapped + * PushBufferStream. + */ public Object getControl(String controlType) { return stream.getControl(controlType); } + /* + * Implements Controls#getControls(). Delegates to the wrapped + * PushBufferStream. + */ public Object[] getControls() { return stream.getControls(); } + /* + * Implements PushBufferStream#getFormat(). Delegates to the wrapped + * PushBufferStream. + */ public Format getFormat() { return stream.getFormat(); } + /* + * Implements SourceStream#endOfStream(). Delegates to the wrapped + * PushBufferStream. + */ public boolean endOfStream() { return stream.endOfStream(); } + /* + * Implements PushBufferStream#read(Buffer). If this instance is muted + * (through its owning MutePushBufferDataSource), overwrites the data + * read from the wrapped PushBufferStream with silence data. + */ public void read(Buffer buffer) throws IOException { stream.read(buffer); @@ -229,47 +276,22 @@ else if (Format.shortArray.equals(dataClass)) } } - public void setTransferHandler(BufferTransferHandler transferHandler) - { - stream.setTransferHandler((transferHandler == null) ? null - : new MuteBufferTransferHandler(transferHandler)); - } - - /** - * Implements a BufferTransferHandler wrapper which doesn't - * expose a wrapped PushBufferStream but rather its wrapper in - * order to give full control to the - * {@link PushBufferStream#read(Buffer)} method of the wrapper. + /* + * Implements PushBufferStream#setTransferHandler(BufferTransferHandler). + * Sets up the hiding of the wrapped PushBufferStream from the specified + * transferHandler and thus gives this MutePushBufferStream full control + * when the transferHandler in question starts calling to the stream + * given to it in BufferTransferHandler#transferData(PushBufferStream). */ - public class MuteBufferTransferHandler - implements BufferTransferHandler + public void setTransferHandler(BufferTransferHandler transferHandler) { - - /** - * The wrapped BufferTransferHandler which receives the - * actual events from the wrapped PushBufferStream. - */ - private final BufferTransferHandler transferHandler; - - /** - * Initializes a new MuteBufferTransferHandler instance - * which is to overwrite the source PushBufferStream of a - * specific BufferTransferHandler. - * - * @param transferHandler the BufferTransferHandler the new - * instance is to overwrite the source - * PushBufferStream of - */ - public MuteBufferTransferHandler( - BufferTransferHandler transferHandler) - { - this.transferHandler = transferHandler; - } - - public void transferData(PushBufferStream stream) - { - transferHandler.transferData(MutePushBufferStream.this); - } + stream.setTransferHandler( + (transferHandler == null) + ? null + : new StreamSubstituteBufferTransferHandler( + transferHandler, + stream, + this)); } } } diff --git a/src/net/java/sip/communicator/impl/media/StreamSubstituteBufferTransferHandler.java b/src/net/java/sip/communicator/impl/media/StreamSubstituteBufferTransferHandler.java new file mode 100644 index 000000000..fd2e7ce4c --- /dev/null +++ b/src/net/java/sip/communicator/impl/media/StreamSubstituteBufferTransferHandler.java @@ -0,0 +1,84 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.media; + +import javax.media.*; +import javax.media.protocol.*; + +/** + * Implements a BufferTransferHandler wrapper which doesn't + * expose a PushBufferStream but rather a specific substitute in order + * to give full control to the {@link PushBufferStream#read(Buffer)} method of + * the substitute. + *

+ * The purpose is achieved in #transferData(PushBufferStream) + * where the method argument stream is ignored and the substitute + * is used instead. + *

+ * + * @author Lubomir Marinov + */ +public class StreamSubstituteBufferTransferHandler + implements BufferTransferHandler +{ + + /** + * The PushBufferStream to be overridden for + * transferHandler with the substitute of this + * instance. + */ + private final PushBufferStream stream; + + /** + * The PushBufferStream to override the stream of + * this instance for transferHandler. + */ + private final PushBufferStream substitute; + + /** + * The wrapped BufferTransferHandler which receives the + * actual events from the wrapped PushBufferStream. + */ + private final BufferTransferHandler transferHandler; + + /** + * Initializes a new StreamSubstituteBufferTransferHandler instance + * which is to overwrite the source PushBufferStream of a specific + * BufferTransferHandler. + * + * @param transferHandler the BufferTransferHandler the new + * instance is to overwrite the source PushBufferStream + * of + * @param stream the PushBufferStream to be overridden for the + * specified transferHandler with the specified + * substitute + * @param substitute the PushBufferStream to override the + * specified stream for the specified + * transferHandler + */ + public StreamSubstituteBufferTransferHandler( + BufferTransferHandler transferHandler, + PushBufferStream stream, + PushBufferStream substitute) + { + this.transferHandler = transferHandler; + this.stream = stream; + this.substitute = substitute; + } + + /* + * Implements BufferTransferHandler#transferData(PushBufferStream). Puts in + * place the essence of the StreamSubstituteBufferTransferHandler class + * which is to report to the transferHandler from the same PushBufferStream + * to which it was set so that the substitute can gain full control. + */ + public void transferData(PushBufferStream stream) + { + transferHandler.transferData( + (stream == this.stream) ? substitute : stream); + } +} diff --git a/src/net/java/sip/communicator/impl/media/conference/AudioMixer.java b/src/net/java/sip/communicator/impl/media/conference/AudioMixer.java new file mode 100644 index 000000000..93a27fd92 --- /dev/null +++ b/src/net/java/sip/communicator/impl/media/conference/AudioMixer.java @@ -0,0 +1,1561 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.media.conference; + +import java.io.*; +import java.lang.reflect.*; +import java.util.*; + +import javax.media.*; +import javax.media.control.*; +import javax.media.format.*; +import javax.media.protocol.*; + +import net.java.sip.communicator.impl.media.*; + +/** + * 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 #addInputDataSource(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 #createOutputDataSource()}. 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 Lubomir 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, + 44100, + 16, + 2, + AudioFormat.LITTLE_ENDIAN, + AudioFormat.SIGNED); + + /** + * 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. + */ + private 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 inputDataSources + = new ArrayList(); + + /** + * The output AudioMixerPushBufferStream through which this + * instance pushes audio sample data to + * AudioMixingPushBufferStreams to be mixed. + */ + private AudioMixerPushBufferStream outputStream; + + /** + * 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) + { + this.captureDevice = captureDevice; + + addInputDataSource((DataSource) captureDevice); + } + + /** + * 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 inputDataSource a new DataSource to input audio to + * this instance + */ + public void addInputDataSource(DataSource inputDataSource) + { + addInputDataSource(inputDataSource, 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 inputDataSource a new DataSource to input audio to + * this instance + * @param outputDataSource the AudioMixingPushBufferDataSource + * to not include the audio contributions of + * inputDataSource in the mix it outputs + */ + void addInputDataSource( + DataSource inputDataSource, + AudioMixingPushBufferDataSource outputDataSource) + { + if (inputDataSource == null) + throw new IllegalArgumentException("inputDataSource"); + + for (InputDataSourceDesc inputDataSourceDesc : inputDataSources) + if (inputDataSource.equals(inputDataSourceDesc.inputDataSource)) + throw new IllegalArgumentException("inputDataSource"); + + inputDataSources.add( + new InputDataSourceDesc( + inputDataSource, + outputDataSource)); + } + + /** + * 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 + */ + void connect() + throws IOException + { + if (connected == 0) + for (InputDataSourceDesc inputDataSourceDesc : inputDataSources) + inputDataSourceDesc.getEffectiveInputDataSource().connect(); + + connected++; + } + + /** + * 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 createOutputDataSource() + { + return new AudioMixingPushBufferDataSource(this); + } + + /** + * Creates a DataSource which attempts to transcode the tracks + * of a specific input DataSource into a specific output + * Format. + * + * @param inputDataSource + * the DataSource from the tracks of which data is + * to be read and transcoded into the specified output + * Format + * @param outputFormat + * the Format in which the tracks of + * inputDataSource are to be transcoded + * @return a new DataSource which attempts to transcode the + * tracks of inputDataSource into + * outputFormat + * @throws IOException + */ + private DataSource createTranscodingDataSource( + DataSource inputDataSource, + Format outputFormat) + throws IOException + { + TranscodingDataSource transcodingDataSource; + + if (inputDataSource instanceof TranscodingDataSource) + transcodingDataSource = null; + else + { + transcodingDataSource + = new TranscodingDataSource(inputDataSource, outputFormat); + + if (connected > 0) + transcodingDataSource.connect(); + } + return transcodingDataSource; + } + + /** + * 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() + { + if (connected <= 0) + return; + + connected--; + + if (connected == 0) + { + outputStream = null; + + for (InputDataSourceDesc inputDataSourceDesc : inputDataSources) + inputDataSourceDesc.getEffectiveInputDataSource().disconnect(); + } + } + + /** + * 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() + { + Time duration = null; + + for (InputDataSourceDesc inputDataSourceDesc : inputDataSources) + { + Time inputDuration + = inputDataSourceDesc + .getEffectiveInputDataSource().getDuration(); + + if (Duration.DURATION_UNBOUNDED.equals(inputDuration) + || Duration.DURATION_UNKNOWN.equals(inputDuration)) + return inputDuration; + + if ((duration == null) + || (duration.getNanoseconds() < inputDuration.getNanoseconds())) + duration = inputDuration; + } + return (duration == null) ? Duration.DURATION_UNKNOWN : duration; + } + + /** + * 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; + } + + /** + * 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() + { + return captureDevice.getFormatControls(); + } + + /** + * Gets the SourceStreams (in the form of + * InputStreamDesc) of a specific DataSource + * (provided in the form of InputDataSourceDesc) which produce + * data in a specific AudioFormat (or a matching one). + * + * @param inputDataSourceDesc + * the DataSource (in the form of + * InputDataSourceDesc) which is to be examined for + * SourceStreams producing data in the specified + * AudioFormat + * @param outputFormat + * the AudioFormat in which the collected + * SourceStreams are to produce data + * @param inputStreams + * the List of InputStreamDesc in which + * the discovered SourceStreams are to be returned + * @return true if SourceStreams produced by the + * specified input DataSource and outputing data in the + * specified AudioFormat were discovered and reported + * in inputStreams; otherwise, false + */ + private boolean getInputStreamsFromInputDataSource( + InputDataSourceDesc inputDataSourceDesc, + AudioFormat outputFormat, + List inputStreams) + { + DataSource inputDataSource + = inputDataSourceDesc.getEffectiveInputDataSource(); + SourceStream[] inputDataSourceStreams; + + if (inputDataSource instanceof PushBufferDataSource) + inputDataSourceStreams + = ((PushBufferDataSource) inputDataSource).getStreams(); + else if (inputDataSource instanceof PullBufferDataSource) + inputDataSourceStreams + = ((PullBufferDataSource) inputDataSource).getStreams(); + else if (inputDataSource instanceof TranscodingDataSource) + inputDataSourceStreams + = ((TranscodingDataSource) inputDataSource).getStreams(); + else + inputDataSourceStreams = null; + + if (inputDataSourceStreams != null) + { + boolean added = false; + + for (SourceStream inputStream : inputDataSourceStreams) + { + Format inputFormat = getFormat(inputStream); + + if ((inputFormat != null) + && matches(inputFormat, outputFormat) + && inputStreams.add( + new InputStreamDesc( + inputStream, + inputDataSourceDesc))) + added = true; + } + return added; + } + + Format inputFormat = getFormat(inputDataSource); + + if ((inputFormat != null) && !matches(inputFormat, outputFormat)) + { + if (inputDataSource instanceof PushDataSource) + { + for (PushSourceStream inputStream + : ((PushDataSource) inputDataSource).getStreams()) + inputStreams.add( + new InputStreamDesc( + new PushBufferStreamAdapter( + inputStream, + inputFormat), + inputDataSourceDesc)); + return true; + } + if (inputDataSource instanceof PullDataSource) + { + for (PullSourceStream inputStream + : ((PullDataSource) inputDataSource).getStreams()) + inputStreams.add( + new InputStreamDesc( + new PullBufferStreamAdapter( + inputStream, + inputFormat), + inputDataSourceDesc)); + return true; + } + } + return false; + } + + /** + * Gets the SourceStreams (in the form of + * InputStreamDesc) 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 outputFormat + * the AudioFormat in which the retrieved + * SourceStreams are to produce data + * @return a new collection of SourceStreams (in the form of + * InputStreamDesc) retrieved from the input + * DataSources of this AudioMixer and + * producing data in the specified AudioFormat + * @throws IOException + */ + private Collection getInputStreamsFromInputDataSources( + AudioFormat outputFormat) + throws IOException + { + List inputStreams = new ArrayList(); + + for (InputDataSourceDesc inputDataSourceDesc : inputDataSources) + { + boolean got + = getInputStreamsFromInputDataSource( + inputDataSourceDesc, + outputFormat, + inputStreams); + + if (!got) + { + DataSource transcodingDataSource + = createTranscodingDataSource( + inputDataSourceDesc.getEffectiveInputDataSource(), + outputFormat); + + if (transcodingDataSource != null) + { + inputDataSourceDesc.setTranscodingDataSource( + transcodingDataSource); + + getInputStreamsFromInputDataSource( + inputDataSourceDesc, + outputFormat, + inputStreams); + } + } + } + return inputStreams; + } + + /** + * 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 getOutputFormatFromInputDataSources() + { + // TODO Auto-generated method stub + return DEFAULT_OUTPUT_FORMAT; + } + + /** + * 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 getOutputStream() + { + AudioFormat outputFormat = getOutputFormatFromInputDataSources(); + + setOutputFormatToInputDataSources(outputFormat); + + Collection inputStreams; + + try + { + inputStreams = getInputStreamsFromInputDataSources(outputFormat); + } + catch (IOException ex) + { + throw new UndeclaredThrowableException(ex); + } + + if (inputStreams.size() <= 0) + outputStream = null; + else + { + if (outputStream == null) + outputStream = new AudioMixerPushBufferStream(outputFormat); + outputStream.setInputStreams(inputStreams); + } + return outputStream; + } + + /** + * 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 an integer from a specific series of bytes starting the reading at + * a specific offset in it. + * + * @param input + * the series of bytes to read an integer from + * @param inputOffset + * the offset in input at which the reading of the + * integer is to start + * @return an integer read from the specified series of bytes starting at + * the specified offset in it + */ + private static int readInt(byte[] input, int inputOffset) + { + return + (input[inputOffset + 3] << 24) + | ((input[inputOffset + 2] & 0xFF) << 16) + | ((input[inputOffset + 1] & 0xFF) << 8) + | (input[inputOffset] & 0xFF); + } + + /** + * Reads a short integer from a specific series of bytes starting the + * reading at a specific offset in it. + * + * @param input + * the series of bytes to read the short integer from + * @param inputOffset + * the offset in input at which the reading of the + * short integer is to start + * @return a short integer in the form of + * int read from the specified series of bytes starting at the specified offset in it + */ + private static int readShort(byte[] input, int inputOffset) + { + return + (short) + ((input[inputOffset + 1] << 8) + | (input[inputOffset] & 0x00FF)); + } + + /** + * 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 outputFormat + * the AudioFormat in which the input + * DataSources of this AudioMixer are + * to be instructed to output + */ + private void setOutputFormatToInputDataSources(AudioFormat outputFormat) + { + String formatControlType = FormatControl.class.getName(); + + for (InputDataSourceDesc inputDataSourceDesc : inputDataSources) + { + DataSource inputDataSource + = inputDataSourceDesc.getEffectiveInputDataSource(); + FormatControl formatControl + = (FormatControl) inputDataSource.getControl(formatControlType); + + if (formatControl != null) + { + Format inputFormat = formatControl.getFormat(); + + if ((inputFormat == null) + || !matches(inputFormat, outputFormat)) + formatControl.setFormat(outputFormat); + } + } + } + + /** + * Starts the input DataSources of this AudioMixer. + * + * @throws IOException + */ + void start() + throws IOException + { + for (InputDataSourceDesc inputDataSourceDesc : inputDataSources) + inputDataSourceDesc.getEffectiveInputDataSource().start(); + } + + /** + * Stops the input DataSources of this AudioMixer. + * + * @throws IOException + */ + void stop() + throws IOException + { + for (InputDataSourceDesc inputDataSourceDesc : inputDataSources) + inputDataSourceDesc.getEffectiveInputDataSource().stop(); + } + + /** + * Represents a PushBufferStream which reads data from the + * SourceStreams of the input DataSources of this + * AudioMixer and pushes it to + * AudioMixingPushBufferStreams for audio mixing. + */ + class AudioMixerPushBufferStream + implements PushBufferStream + { + + /** + * The factor which scales a short value to an int + * value. + */ + private static final float INT_TO_SHORT_RATIO + = Integer.MAX_VALUE / (float) Short.MAX_VALUE; + + /** + * The factor which scales an int value to a short + * value. + */ + private static final float SHORT_TO_INT_RATIO + = Short.MAX_VALUE / (float) Integer.MAX_VALUE; + + /** + * The SourceStreams (in the form of + * InputStreamDesc so that this instance can track back the + * AudioMixingPushBufferDataSource which outputs the mixed + * audio stream and determine whether the associated + * SourceStream is to be included into the mix) from which + * this instance reads its data. + */ + private InputStreamDesc[] inputStreams; + + /** + * The AudioFormat of the data this instance outputs. + */ + private final AudioFormat outputFormat; + + /** + * The AudioMixingPushBufferStreams to which this instance + * pushes data for audio mixing. + */ + private final List outputStreams + = new ArrayList(); + + /** + * The BufferTransferHandler through which this instance + * gets notifications from its input SourceStreams that new + * data is available for audio mixing. + */ + private final BufferTransferHandler transferHandler + = new BufferTransferHandler() + { + public void transferData(PushBufferStream stream) + { + AudioMixerPushBufferStream.this.transferData(); + } + }; + + /** + * Initializes a new AudioMixerPushBufferStream instance to + * output data in a specific AudioFormat. + * + * @param outputFormat + * the AudioFormat in which the new instance is + * to output data + */ + private AudioMixerPushBufferStream(AudioFormat outputFormat) + { + this.outputFormat = outputFormat; + } + + /** + * Adds a specific AudioMixingPushBufferStream to the + * collection of such streams which this instance is to push the data + * for audio mixing it reads from its input SourceStreams. + * + * @param outputStream + * the AudioMixingPushBufferStream to be added + * to the collection of such streams which this instance is + * to push the data for audio mixing it reads from its input + * SourceStreams + */ + void addOutputStream(AudioMixingPushBufferStream outputStream) + { + if (outputStream == null) + throw new IllegalArgumentException("outputStream"); + + synchronized (outputStreams) + { + if (!outputStreams.contains(outputStream)) + outputStreams.add(outputStream); + } + } + + /* + * Implements SourceStream#endOfStream(). Delegates to the input + * SourceStreams of this instance. + */ + public boolean endOfStream() + { + if (inputStreams != null) + for (InputStreamDesc inputStreamDesc : inputStreams) + if (!inputStreamDesc.getInputStream().endOfStream()) + return false; + return true; + } + + /* + * Implements SourceStream#getContentDescriptor(). Returns a + * ContentDescriptor which describes the content type of this instance. + */ + public ContentDescriptor getContentDescriptor() + { + return + new ContentDescriptor(AudioMixer.this.getContentType()); + } + + /* + * Implements SourceStream#getContentLength(). Delegates to the input + * SourceStreams of this instance. + */ + public long getContentLength() + { + long contentLength = 0; + + if (inputStreams != null) + for (InputStreamDesc inputStreamDesc : inputStreams) + { + long inputContentLength + = inputStreamDesc.getInputStream().getContentLength(); + + if (LENGTH_UNKNOWN == inputContentLength) + return LENGTH_UNKNOWN; + if (contentLength < inputContentLength) + contentLength = inputContentLength; + } + return contentLength; + } + + /* + * Implements Controls#getControl(String). Does nothing. + */ + public Object getControl(String controlType) + { + // TODO Auto-generated method stub + return null; + } + + /* + * Implements Controls#getControls(). Does nothing. + */ + public Object[] getControls() + { + // TODO Auto-generated method stub + return new Object[0]; + } + + /* + * Implements PushBufferStream#getFormat(). Returns the output + * AudioFormat in which this instance was configured to output its data. + */ + public AudioFormat getFormat() + { + return outputFormat; + } + + /* + * Implements PushBufferStream#read(Buffer). Reads audio samples from + * the input SourceStreams of this instance in various formats, converts + * the read audio samples to one and the same format and pushes them to + * the output AudioMixingPushBufferStreams for the very audio mixing. + */ + public void read(Buffer buffer) + throws IOException + { + int inputStreamCount + = (inputStreams == null) ? 0 : inputStreams.length; + + if (inputStreamCount <= 0) + return; + + AudioFormat outputFormat = getFormat(); + int[][] inputSamples = new int[inputStreamCount][]; + int maxInputSampleCount; + + try + { + maxInputSampleCount + = readInputPushBufferStreams(outputFormat, inputSamples); + } + catch (UnsupportedFormatException ufex) + { + IOException ioex = new IOException(); + ioex.initCause(ufex); + throw ioex; + } + + maxInputSampleCount + = Math.max( + maxInputSampleCount, + readInputPullBufferStreams( + outputFormat, + maxInputSampleCount, + inputSamples)); + + buffer.setData(inputSamples); + buffer.setLength(maxInputSampleCount); + } + + /** + * Reads audio samples from a specific PushBufferStream and + * converts them to a specific output AudioFormat. An + * attempt is made to read a specific maximum number of samples from the + * specified PushBufferStream but the very + * PushBufferStream may not honor the request. + * + * @param inputStream + * the PushBufferStream to read from + * @param outputFormat + * the AudioFormat to which the samples read + * from inputStream are to converted before + * being returned + * @param sampleCount + * the maximum number of samples which the read operation + * should attempt to read from inputStream but + * the very inputStream may not honor the + * request + * @return + * @throws IOException + * @throws UnsupportedFormatException + */ + private int[] read( + PushBufferStream inputStream, + AudioFormat outputFormat, + int sampleCount) + throws IOException, + UnsupportedFormatException + { + Buffer buffer = new Buffer(); + + if (sampleCount != 0) + { + AudioFormat inputFormat = (AudioFormat) inputStream.getFormat(); + Class inputDataType = inputFormat.getDataType(); + + if (Format.byteArray.equals(inputDataType)) + { + buffer.setData( + new byte[ + sampleCount + * (inputFormat.getSampleSizeInBits() / 8)]); + buffer.setLength(0); + buffer.setOffset(0); + } + else + throw + new UnsupportedFormatException( + "!Format.getDataType().equals(byte[].class)", + inputFormat); + } + + inputStream.read(buffer); + + int inputLength = buffer.getLength(); + + if (inputLength <= 0) + return null; + + AudioFormat inputFormat = (AudioFormat) buffer.getFormat(); + + if (inputFormat.getSigned() != AudioFormat.SIGNED) + throw + new UnsupportedFormatException( + "AudioFormat.getSigned()", + inputFormat); + if (inputFormat.getChannels() != outputFormat.getChannels()) + throw + new UnsupportedFormatException( + "AudioFormat.getChannels()", + inputFormat); + + Object inputData = buffer.getData(); + + if (inputData instanceof byte[]) + { + byte[] inputSamples = (byte[]) inputData; + int[] outputSamples; + int outputSampleSizeInBits = outputFormat.getSampleSizeInBits(); + + switch (inputFormat.getSampleSizeInBits()) + { + case 16: + outputSamples = new int[inputSamples.length / 2]; + for (int i = 0; i < outputSamples.length; i++) + { + int sample = readShort(inputSamples, i * 2); + + switch (outputSampleSizeInBits) + { + case 16: + break; + case 32: + sample = Math.round(sample * INT_TO_SHORT_RATIO); + break; + case 8: + case 24: + default: + throw + new UnsupportedFormatException( + "AudioFormat.getSampleSizeInBits()", + outputFormat); + } + + outputSamples[i] = sample; + } + return outputSamples; + case 32: + outputSamples = new int[inputSamples.length / 4]; + for (int i = 0; i < outputSamples.length; i++) + { + int sample = readInt(inputSamples, i * 4); + + switch (outputSampleSizeInBits) + { + case 16: + sample = Math.round(sample * SHORT_TO_INT_RATIO); + break; + case 32: + break; + case 8: + case 24: + default: + throw + new UnsupportedFormatException( + "AudioFormat.getSampleSizeInBits()", + outputFormat); + } + + outputSamples[i] = sample; + } + return outputSamples; + case 8: + case 24: + default: + throw + new UnsupportedFormatException( + "AudioFormat.getSampleSizeInBits()", + inputFormat); + } + } + else if (inputData instanceof short[]) + { + throw + new UnsupportedFormatException( + "Format.getDataType().equals(short[].class)", + inputFormat); + } + else if (inputData instanceof int[]) + { + throw + new UnsupportedFormatException( + "Format.getDataType().equals(int[].class)", + inputFormat); + } + return null; + } + + /** + * Reads audio samples from the input PullBufferStreams of + * this instance and converts them to a specific output + * AudioFormat. An attempt is made to read a specific + * maximum number of samples from each of the + * PullBufferStreams but the very + * PullBufferStream may not honor the request. + * + * @param outputFormat + * the AudioFormat in which the audio samples + * read from the PullBufferStreams are to be + * converted before being returned + * @param outputSampleCount + * the maximum number of audio samples to be read from each + * of the PullBufferStreams but the very + * PullBufferStream may not honor the request + * @param inputSamples + * the collection of audio samples in which the read audio + * samples are to be returned + * @return the maximum number of audio samples actually read from the + * input PullBufferStreams of this instance + * @throws IOException + */ + private int readInputPullBufferStreams( + AudioFormat outputFormat, + int outputSampleCount, + int[][] inputSamples) + throws IOException + { + int maxInputSampleCount = 0; + + for (InputStreamDesc inputStreamDesc : inputStreams) + if (inputStreamDesc.getInputStream() instanceof PullBufferStream) + throw + new UnsupportedOperationException( + AudioMixerPushBufferStream.class.getSimpleName() + + ".readInputPullBufferStreams(AudioFormat,int,int[][])"); + return maxInputSampleCount; + } + + /** + * Reads audio samples from the input PushBufferStreams of + * this instance and converts them to a specific output + * AudioFormat. + * + * @param outputFormat + * the AudioFormat in which the audio samples + * read from the PushBufferStreams are to be + * converted before being returned + * @param inputSamples + * the collection of audio samples in which the read audio + * samples are to be returned + * @return the maximum number of audio samples actually read from the + * input PushBufferStreams of this instance + * @throws IOException + * @throws UnsupportedFormatException + */ + private int readInputPushBufferStreams( + AudioFormat outputFormat, + int[][] inputSamples) + throws IOException, + UnsupportedFormatException + { + int maxInputSampleCount = 0; + + for (int i = 0; i < inputStreams.length; i++) + { + SourceStream inputStream = inputStreams[i].getInputStream(); + + if (inputStream instanceof PushBufferStream) + { + int[] inputStreamSamples + = read( + (PushBufferStream) inputStream, + outputFormat, + maxInputSampleCount); + + if (inputStreamSamples != null) + { + int inputStreamSampleCount = inputStreamSamples.length; + + if (inputStreamSampleCount != 0) + { + inputSamples[i] = inputStreamSamples; + + if (maxInputSampleCount < inputStreamSampleCount) + maxInputSampleCount = inputStreamSampleCount; + } + } + } + } + return maxInputSampleCount; + } + + /** + * Removes a specific AudioMixingPushBufferStream from the + * collection of such streams which this instance pushes the data for + * audio mixing it reads from its input SourceStreams. + * + * @param outputStream + * the AudioMixingPushBufferStream to be removed + * from the collection of such streams which this instance + * pushes the data for audio mixing it reads from its input + * SourceStreams + */ + void removeOutputStream(AudioMixingPushBufferStream outputStream) + { + synchronized (outputStreams) + { + if (outputStream != null) + outputStreams.remove(outputStream); + } + } + + /** + * Pushes a copy of a specific set of input audio samples to a specific + * AudioMixingPushBufferStream for audio mixing. Audio + * samples read from input DataSources which the + * AudioMixingPushBufferDataSource owner of the specified + * AudioMixingPushBufferStream has specified to not be + * included in the output mix are not pushed to the + * AudioMixingPushBufferStream. + * + * @param outputStream + * the AudioMixingPushBufferStream to push the + * specified set of audio samples to + * @param inputSamples + * the set of audio samples to be pushed to + * outputStream for audio mixing + * @param maxInputSampleCount + * the maximum number of audio samples available in + * inputSamples + */ + private void setInputSamples( + AudioMixingPushBufferStream outputStream, + int[][] inputSamples, + int maxInputSampleCount) + { + inputSamples = inputSamples.clone(); + + AudioMixingPushBufferDataSource outputDataSource + = outputStream.getDataSource(); + + for (int i = 0; i < inputSamples.length; i++) + { + InputStreamDesc inputStreamDesc = inputStreams[i]; + + if (outputDataSource.equals( + inputStreamDesc.getOutputDataSource())) + inputSamples[i] = null; + } + + outputStream.setInputSamples(inputSamples, maxInputSampleCount); + } + + /** + * Sets the SourceStreams (in the form of + * InputStreamDesc) from which this instance is to read + * audio samples and push them to the + * AudioMixingPushBufferStreams for audio mixing. + * + * @param inputStreams + * the SourceStreams (in the form of + * InputStreamDesc) from which this instance is + * to read audio samples and push them to the + * AudioMixingPushBufferStreams for audio mixing + */ + private void setInputStreams(Collection inputStreams) + { + InputStreamDesc[] oldValue = this.inputStreams; + InputStreamDesc[] newValue + = inputStreams.toArray( + new InputStreamDesc[inputStreams.size()]); + boolean valueIsChanged = !Arrays.equals(oldValue, newValue); + + if (valueIsChanged) + setTransferHandler(oldValue, null); + this.inputStreams = newValue; + if (valueIsChanged) + { + boolean skippedForTransferHandler = false; + + for (InputStreamDesc inputStreamDesc : newValue) + { + SourceStream inputStream = inputStreamDesc.getInputStream(); + + if (inputStream instanceof PushBufferStream) + { + if (!skippedForTransferHandler) + skippedForTransferHandler = true; + else if (!(inputStream instanceof CachingPushBufferStream)) + inputStreamDesc.setInputStream( + new CachingPushBufferStream( + (PushBufferStream) inputStream)); + } + } + + setTransferHandler(newValue, transferHandler); + } + } + + /* + * Implements PushBufferStream#setTransferHandler(BufferTransferHandler). + * Because this instance pushes data to multiple output + * AudioMixingPushBufferStreams, a single BufferTransferHandler is not + * sufficient and thus this method is unsupported. + */ + public void setTransferHandler(BufferTransferHandler transferHandler) + { + throw + new UnsupportedOperationException( + AudioMixerPushBufferStream.class.getSimpleName() + + ".setTransferHandler(BufferTransferHandler)"); + } + + /** + * Sets a specific BufferTransferHandler to a specific + * collection of SourceStreams (in the form of + * InputStreamDesc) abstracting the differences among the + * various types of SourceStreams. + * + * @param inputStreams + * the input SourceStreams to which the + * specified BufferTransferHandler is to be set + * @param transferHandler + * the BufferTransferHandler to be set to the + * specified inputStreams + */ + private void setTransferHandler( + InputStreamDesc[] inputStreams, + BufferTransferHandler transferHandler) + { + if ((inputStreams == null) || (inputStreams.length <= 0)) + return; + + boolean transferHandlerIsSet = false; + + for (InputStreamDesc inputStreamDesc : inputStreams) + { + SourceStream inputStream = inputStreamDesc.getInputStream(); + + if (inputStream instanceof PushBufferStream) + { + BufferTransferHandler inputStreamTransferHandler; + PushBufferStream inputPushBufferStream + = (PushBufferStream) inputStream; + + if (transferHandler == null) + inputStreamTransferHandler = null; + else if (transferHandlerIsSet) + inputStreamTransferHandler + = new BufferTransferHandler() + { + public void transferData( + PushBufferStream stream) + { + /* + * Do nothing because we don't want + * the associated PushBufferStream + * to cause the transfer of data + * from this + * AudioMixerPushBufferStream. + */ + } + }; + else + inputStreamTransferHandler + = new StreamSubstituteBufferTransferHandler( + transferHandler, + inputPushBufferStream, + this); + + inputPushBufferStream.setTransferHandler( + inputStreamTransferHandler); + + transferHandlerIsSet = true; + } + } + } + + /** + * Reads audio samples from the input SourceStreams of this + * instance and pushes them to its output + * AudioMixingPushBufferStreams for audio mixing. + */ + protected void transferData() + { + Buffer buffer = new Buffer(); + + try + { + read(buffer); + } + catch (IOException ex) + { + throw new UndeclaredThrowableException(ex); + } + + int[][] inputSamples = (int[][]) buffer.getData(); + int maxInputSampleCount = buffer.getLength(); + + if ((inputSamples == null) + || (inputSamples.length == 0) + || (maxInputSampleCount <= 0)) + return; + + AudioMixingPushBufferStream[] outputStreams; + + synchronized (this.outputStreams) + { + outputStreams + = this.outputStreams.toArray( + new AudioMixingPushBufferStream[ + this.outputStreams.size()]); + } + for (AudioMixingPushBufferStream outputStream : outputStreams) + setInputSamples(outputStream, inputSamples, maxInputSampleCount); + } + } + + /** + * Describes additional information about a specific input + * DataSource of an AudioMixer so that the + * AudioMixer can, for example, quickly discover the output + * AudioMixingPushBufferDataSource in the mix of which the + * contribution of the DataSource is to not be included. + */ + private static class InputDataSourceDesc + { + + /** + * The DataSource for which additional information is + * described by this instance. + */ + public final DataSource inputDataSource; + + /** + * The AudioMixingPushBufferDataSource in which the + * mix contributions of the DataSource described by this + * instance are to not be included. + */ + public final AudioMixingPushBufferDataSource outputDataSource; + + /** + * The DataSource, if any, which transcodes the tracks of + * inputDataSource in the output Format of the + * associated AudioMixer. + */ + private DataSource transcodingDataSource; + + /** + * Initializes a new InputDataSourceDesc instance which is + * to describe additional information about a specific input + * DataSource of an AudioMixer. Associates the + * specified DataSource with the + * AudioMixingPushBufferDataSource in which the mix + * contributions of the specified input DataSource are to + * not be included. + * + * @param inputDataSource + * a DataSourc for which additional information + * is to be described by the new instance + * @param outputDataSource + * the AudioMixingPushBufferDataSource in which + * the mix contributions of inputDataSource are + * to not be included + */ + public InputDataSourceDesc( + DataSource inputDataSource, + AudioMixingPushBufferDataSource outputDataSource) + { + this.inputDataSource = inputDataSource; + this.outputDataSource = outputDataSource; + } + + /** + * Gets the actual DataSource from which the associated + * AudioMixer directly reads in order to retrieve the mix + * contribution of the DataSource described by this + * instance. + * + * @return the actual DataSource from which the associated + * AudioMixer directly reads in order to retrieve + * the mix contribution of the DataSource described + * by this instance + */ + public DataSource getEffectiveInputDataSource() + { + return + (transcodingDataSource == null) + ? inputDataSource + : transcodingDataSource; + } + + /** + * Sets the DataSource, if any, which transcodes the tracks + * of the input DataSource described by this instance in + * the output Format of the associated + * AudioMixer. + * + * @param transcodingDataSource + * the DataSource which transcodes the tracks of + * the input DataSource described by this + * instance in the output Format of the + * associated AudioMixer + */ + public void setTranscodingDataSource(DataSource transcodingDataSource) + { + this.transcodingDataSource = transcodingDataSource; + } + } + + /** + * Describes additional information about a specific input audio + * SourceStream of an AudioMixer so that the + * AudioMixer can, for example, quickly discover the output + * AudioMixingPushBufferDataSource in the mix of which the + * contribution of the SourceStream is to not be included. + */ + private static class InputStreamDesc + { + + /** + * The DataSource which created the + * SourceStream described by this instance and additional + * information about it. + */ + private final InputDataSourceDesc inputDataSourceDesc; + + /** + * The SourceStream for which additional information is + * described by this instance. + */ + private SourceStream inputStream; + + /** + * Initializes a new InputStreamDesc instance which is to + * describe additional information about a specific input audio + * SourceStream of an AudioMixer. Associates + * the specified SourceStream with the + * DataSource which created it and additional information + * about it. + * + * @param inputStream + * a SourceStream for which additional + * information is to be described by the new instance + * @param inputDataSourceDesc + * the DataSource which created the + * SourceStream to be described by the new + * instance and additional information about it + */ + public InputStreamDesc( + SourceStream inputStream, + InputDataSourceDesc inputDataSourceDesc) + { + this.inputStream = inputStream; + this.inputDataSourceDesc = inputDataSourceDesc; + } + + /** + * Gets the SourceStream described by this instance + * + * @return the SourceStream described by this instance + */ + public SourceStream getInputStream() + { + return inputStream; + } + + /** + * Gets the AudioMixingPushBufferDataSource in which the + * mix contribution of the SourceStream described by this + * instance is to not be included. + * + * @return the AudioMixingPushBufferDataSource in which the + * mix contribution of the SourceStream described + * by this instance is to not be included + */ + public AudioMixingPushBufferDataSource getOutputDataSource() + { + return inputDataSourceDesc.outputDataSource; + } + + /** + * Sets the SourceStream to be described by this instance + * + * @param inputStream + * the SourceStream to be described by this + * instance + */ + public void setInputStream(SourceStream inputStream) + { + this.inputStream = inputStream; + } + } +} diff --git a/src/net/java/sip/communicator/impl/media/conference/AudioMixingPushBufferDataSource.java b/src/net/java/sip/communicator/impl/media/conference/AudioMixingPushBufferDataSource.java new file mode 100644 index 000000000..faa7072a2 --- /dev/null +++ b/src/net/java/sip/communicator/impl/media/conference/AudioMixingPushBufferDataSource.java @@ -0,0 +1,239 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.media.conference; + +import java.io.*; +import java.lang.reflect.*; + +import javax.media.*; +import javax.media.control.*; +import javax.media.protocol.*; + +/** + * Represents a PushBufferDataSource which provides a single + * PushBufferStream containing the result of the audio mixing of + * DataSources. + * + * @author Lubomir Marinov + */ +public class AudioMixingPushBufferDataSource + extends PushBufferDataSource + implements CaptureDevice +{ + + /** + * The AudioMixer performing the audio mixing, managing the + * input DataSources and pushing the data of this output + * PushBufferDataSource. + */ + private final AudioMixer audioMixer; + + /** + * The indicator which determines whether this DataSource is + * connected. + */ + private boolean connected; + + /** + * The one and only PushBufferStream this + * PushBufferDataSource provides to its clients and containing + * the result of the audio mixing performed by audioMixer. + */ + private AudioMixingPushBufferStream outputStream; + + /** + * The indicator which determines whether this DataSource is + * started. + */ + private boolean started; + + /** + * Initializes a new AudioMixingPushBufferDataSource instance + * which gives access to the result of the audio mixing performed by a + * specific AudioMixer. + * + * @param audioMixer the AudioMixer performing audio mixing, + * managing the input DataSources and pushing the + * data of the new output PushBufferDataSource + */ + public AudioMixingPushBufferDataSource(AudioMixer audioMixer) + { + this.audioMixer = audioMixer; + } + + /** + * Adds a new input DataSource to be mixed by the associated + * AudioMixer of this instance and to not have its audio + * contributions included in the mixing output represented by this + * DataSource. + * + * @param inputDataSource a DataSource to be added for mixing + * to the AudioMixer associate with this instance + * and to not have its audio contributions included in the mixing + * output represented by this DataSource + */ + public void addInputDataSource(DataSource inputDataSource) + { + audioMixer.addInputDataSource(inputDataSource, this); + } + + /* + * Implements DataSource#connect(). Lets the AudioMixer know that one of its + * output PushBufferDataSources has been connected and marks this DataSource + * as connected. + */ + public void connect() + throws IOException + { + if (!connected) + { + audioMixer.connect(); + connected = true; + } + } + + /* + * Implements DataSource#disconnect(). Marks this DataSource as disconnected + * and notifies the AudioMixer that one of its output PushBufferDataSources + * has been disconnected. + */ + public void disconnect() + { + try + { + stop(); + } + catch (IOException ex) + { + throw new UndeclaredThrowableException(ex); + } + + if (connected) + { + outputStream = null; + connected = false; + + audioMixer.disconnect(); + } + } + + /* + * Implements CaptureDevice#getCaptureDeviceInfo(). Delegates to the + * associated AudioMixer because it knows which CaptureDevice is being + * wrapped. + */ + public CaptureDeviceInfo getCaptureDeviceInfo() + { + return audioMixer.getCaptureDeviceInfo(); + } + + /* + * Implements DataSource#getContentType(). Delegates to the associated + * AudioMixer because it manages the inputs and knows their characteristics. + */ + public String getContentType() + { + return audioMixer.getContentType(); + } + + /* + * Implements DataSource#getControl(String). Does nothing. + */ + public Object getControl(String controlType) + { + // TODO Auto-generated method stub + return null; + } + + /* + * Implements DataSource#getControls(). Does nothing. + */ + public Object[] getControls() + { + // TODO Auto-generated method stub + return new Object[0]; + } + + /* + * Implements DataSource#getDuration(). Delegates to the associated + * AudioMixer because it manages the inputs and knows their characteristics. + */ + public Time getDuration() + { + return audioMixer.getDuration(); + } + + /* + * Implements CaptureDevice#getFormatControls(). Delegates to the associated + * AudioMixer because it knows which CaptureDevice is being wrapped. + */ + public FormatControl[] getFormatControls() + { + return audioMixer.getFormatControls(); + } + + /* + * Implements PushBufferDataSource#getStreams(). Gets a PushBufferStream + * which reads data from the associated AudioMixer and mixes it. + */ + public PushBufferStream[] getStreams() + { + if (outputStream == null) + { + AudioMixer.AudioMixerPushBufferStream audioMixerOutputStream + = audioMixer.getOutputStream(); + + if (audioMixerOutputStream != null) + { + outputStream + = new AudioMixingPushBufferStream( + audioMixerOutputStream, + this); + if (started) + outputStream.start(); + } + } + return + (outputStream == null) + ? new PushBufferStream[0] + : new PushBufferStream[] { outputStream }; + } + + /* + * Implements DataSource#start(). Starts the output PushBufferStream of + * this DataSource (if it exists) and notifies the AudioMixer that one of + * its output PushBufferDataSources has been started. + */ + public void start() + throws IOException + { + if (!started) + { + if (outputStream != null) + outputStream.start(); + audioMixer.start(); + started = true; + } + } + + /* + * Implements DataSource#stop(). Notifies the AudioMixer that one of its + * output PushBufferDataSources has been stopped and stops the output + * PushBufferStream of this DataSource (if it exists). + */ + public void stop() + throws IOException + { + if (started) + { + audioMixer.stop(); + if (outputStream != null) + outputStream.stop(); + started = false; + } + } +} diff --git a/src/net/java/sip/communicator/impl/media/conference/AudioMixingPushBufferStream.java b/src/net/java/sip/communicator/impl/media/conference/AudioMixingPushBufferStream.java new file mode 100644 index 000000000..ad28c1da1 --- /dev/null +++ b/src/net/java/sip/communicator/impl/media/conference/AudioMixingPushBufferStream.java @@ -0,0 +1,384 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.media.conference; + +import java.io.*; + +import javax.media.*; +import javax.media.format.*; +import javax.media.protocol.*; + +/** + * Represents a PushBufferStream containing the result of the audio + * mixing of DataSources. + * + * @author Lubomir Marinov + */ +public class AudioMixingPushBufferStream + implements PushBufferStream +{ + + /** + * The AudioMixer.AudioMixerPushBufferStream which reads data + * from the input DataSources and pushes it to this instance to + * be mixed. + */ + private final AudioMixer.AudioMixerPushBufferStream audioMixerStream; + + /** + * The AudioMixingPushBufferDataSource which created and owns + * this instance and defines the input data which is to not be mixed in the + * output of this PushBufferStream. + */ + private final AudioMixingPushBufferDataSource dataSource; + + /** + * The collection of input audio samples still not mixed and read through + * this AudioMixingPushBufferStream. + */ + private int[][] inputSamples; + + /** + * The maximum number of per-stream audio samples available through + * inputSamples. + */ + private int maxInputSampleCount; + + /** + * The BufferTransferHandler through which this + * PushBufferStream notifies its clients that new data is + * available for reading. + */ + private BufferTransferHandler transferHandler; + + /** + * Initializes a new AudioMixingPushBufferStream mixing the + * input data of a specific + * AudioMixer.AudioMixerPushBufferStream and excluding from the + * mix the audio contributions of a specific + * AudioMixingPushBufferDataSource. + * + * @param audioMixerStream the + * AudioMixer.AudioMixerPushBufferStream reading + * data from input DataSources and to push it to the + * new AudioMixingPushBufferStream + * @param dataSource the AudioMixingPushBufferDataSource which + * has requested the initialization of the new instance and which + * defines the input data to not be mixed in the output of the + * new instance + */ + public AudioMixingPushBufferStream( + AudioMixer.AudioMixerPushBufferStream audioMixerStream, + AudioMixingPushBufferDataSource dataSource) + { + this.audioMixerStream = audioMixerStream; + this.dataSource = dataSource; + } + + /* + * Implements SourceStream#endOfStream(). Delegates to the wrapped + * AudioMixer.AudioMixerPushBufferStream. + */ + public boolean endOfStream() + { + /* + * TODO If the inputSamples haven't been consumed yet, don't report the + * end of this stream even if the wrapped stream has reached its end. + */ + return audioMixerStream.endOfStream(); + } + + /* + * Implements SourceStream#getContentDescriptor(). Delegates to the wrapped + * AudioMixer.AudioMixerPushBufferStream. + */ + public ContentDescriptor getContentDescriptor() + { + return audioMixerStream.getContentDescriptor(); + } + + /* + * Implements SourceStream#getContentLength(). Delegates to the wrapped + * AudioMixer.AudioMixerPushBufferStream. + */ + public long getContentLength() + { + return audioMixerStream.getContentLength(); + } + + /* + * Implements Controls#getControl(String). Does nothing. + */ + public Object getControl(String controlType) + { + // TODO Auto-generated method stub + return null; + } + + /* + * Implements Controls#getControls(). Does nothing. + */ + public Object[] getControls() + { + // TODO Auto-generated method stub + return new Object[0]; + } + + /** + * Gets the AudioMixingPushBufferDataSource which created and + * owns this instance and defines the input data which is to not be mixed in + * the output of this PushBufferStream. + * + * @return the AudioMixingPushBufferDataSource which created + * and owns this instance and defines the input data which is to not + * be mixed in the output of this PushBufferStream + */ + public AudioMixingPushBufferDataSource getDataSource() + { + return dataSource; + } + + /* + * Implements PushBufferStream#getFormat(). Delegates to the wrapped + * AudioMixer.AudioMixerPushBufferStream. + */ + public AudioFormat getFormat() + { + return audioMixerStream.getFormat(); + } + + /** + * Gets the maximum possible value for an audio sample of a specific + * AudioFormat. + * + * @param outputFormat the AudioFormat of which to get the + * maximum possible value for an audio sample + * @return the maximum possible value for an audio sample of the specified + * AudioFormat + * @throws UnsupportedFormatException + */ + private static int getMaxOutputSample(AudioFormat outputFormat) + throws UnsupportedFormatException + { + switch(outputFormat.getSampleSizeInBits()) + { + case 8: + return Byte.MAX_VALUE; + case 16: + return Short.MAX_VALUE; + case 32: + return Integer.MAX_VALUE; + case 24: + default: + throw + new UnsupportedFormatException( + "Format.getSampleSizeInBits()", + outputFormat); + } + } + + /** + * Mixes as in audio mixing a specified collection of audio sample sets and + * returns the resulting mix audio sample set in a specific + * AudioFormat. + * + * @param inputSamples the collection of audio sample sets to be mixed into + * one audio sample set in the sense of audio mixing + * @param outputFormat the AudioFormat in which the resulting + * mix audio sample set is to be produced + * @param outputSampleCount the size of the resulting mix audio sample set + * to be produced + * @return the resulting audio sample set of the audio mixing of the + * specified input audio sample sets + */ + private static int[] mix( + int[][] inputSamples, + AudioFormat outputFormat, + int outputSampleCount) + { + int[] outputSamples = new int[outputSampleCount]; + int maxOutputSample; + + try + { + maxOutputSample = getMaxOutputSample(outputFormat); + } + catch (UnsupportedFormatException ufex) + { + throw new UnsupportedOperationException(ufex); + } + + for (int[] inputStreamSamples : inputSamples) + { + + if (inputStreamSamples == null) + continue; + + int inputStreamSampleCount = inputStreamSamples.length; + + if (inputStreamSampleCount <= 0) + continue; + + for (int i = 0; i < inputStreamSampleCount; i++) + { + int inputStreamSample = inputStreamSamples[i]; + int outputSample = outputSamples[i]; + + outputSamples[i] + = inputStreamSample + + outputSample + - Math.round( + inputStreamSample + * (outputSample + / (float) maxOutputSample)); + } + } + return outputSamples; + } + + /* + * Implements PushBufferStream#read(Buffer). If inputSamples are available, + * mixes them and writes them to the specified Buffer performing the + * necessary data type conversions. + */ + public void read(Buffer buffer) + throws IOException + { + int[][] inputSamples = this.inputSamples; + int maxInputSampleCount = this.maxInputSampleCount; + + this.inputSamples = null; + this.maxInputSampleCount = 0; + + if ((inputSamples == null) + || (inputSamples.length == 0) + || (maxInputSampleCount <= 0)) + return; + + AudioFormat outputFormat = getFormat(); + int[] outputSamples + = mix(inputSamples, outputFormat, maxInputSampleCount); + + Class outputDataType = outputFormat.getDataType(); + + if (Format.byteArray.equals(outputDataType)) + { + byte[] outputData; + + switch (outputFormat.getSampleSizeInBits()) + { + case 16: + outputData = new byte[outputSamples.length * 2]; + for (int i = 0; i < outputSamples.length; i++) + writeShort(outputSamples[i], outputData, i * 2); + break; + case 32: + outputData = new byte[outputSamples.length * 4]; + for (int i = 0; i < outputSamples.length; i++) + writeInt(outputSamples[i], outputData, i * 4); + break; + case 8: + case 24: + default: + throw + new UnsupportedOperationException( + "AudioMixingPushBufferStream.read(Buffer)"); + } + + buffer.setData(outputData); + buffer.setFormat(outputFormat); + buffer.setLength(outputData.length); + buffer.setOffset(0); + } + else + throw + new UnsupportedOperationException( + "AudioMixingPushBufferStream.read(Buffer)"); + } + + /** + * Sets the collection of audio sample sets to be mixed in the sense of + * audio mixing by this stream when data is read from it. Triggers a push to + * the clients of this stream. + * + * @param inputSamples the collection of audio sample sets to be mixed by + * this stream when data is read from it + * @param maxInputSampleCount the maximum number of per-stream audio samples + * available through inputSamples + */ + void setInputSamples(int[][] inputSamples, int maxInputSampleCount) + { + this.inputSamples = inputSamples; + this.maxInputSampleCount = maxInputSampleCount; + + if (transferHandler != null) + transferHandler.transferData(this); + } + + /* + * Implements PushBufferStream#setTransferHandler(BufferTransferHandler). + */ + public void setTransferHandler(BufferTransferHandler transferHandler) + { + this.transferHandler = transferHandler; + } + + /** + * Starts the pushing of data out of this stream. + */ + void start() + { + audioMixerStream.addOutputStream(this); + } + + /** + * Stops the pushing of data out of this stream. + */ + void stop() + { + audioMixerStream.removeOutputStream(this); + } + + /** + * Converts an integer to a series of bytes and writes the result into a + * specific output array of bytes starting the writing at a specific offset + * in it. + * + * @param input the integer to be written out as a series of bytes + * @param output the output to receive the conversion of the specified + * integer to a series of bytes + * @param outputOffset the offset in output at which the + * writing of the result of the conversion is to be started + */ + private static void writeInt(int input, byte[] output, int outputOffset) + { + output[outputOffset] = (byte) (input & 0xFF); + output[outputOffset + 1] = (byte) ((input >>> 8) & 0xFF); + output[outputOffset + 2] = (byte) ((input >>> 16) & 0xFF); + output[outputOffset + 3] = (byte) (input >> 24); + } + + /** + * Converts a short integer to a series of bytes and writes the result into + * a specific output array of bytes starting the writing at a specific + * offset in it. + * + * @param input the short integer to be written out as a series of bytes + * specified as an integer i.e. the value to be converted is + * contained in only two of the four bytes made available by the + * integer + * @param output the output to receive the conversion of the specified + * short integer to a series of bytes + * @param outputOffset the offset in output at which the + * writing of the result of the conversion is to be started + */ + private static void writeShort(int input, byte[] output, int outputOffset) + { + output[outputOffset] = (byte) (input & 0xFF); + output[outputOffset + 1] = (byte) (input >> 8); + } +} diff --git a/src/net/java/sip/communicator/impl/media/conference/BufferStreamAdapter.java b/src/net/java/sip/communicator/impl/media/conference/BufferStreamAdapter.java new file mode 100644 index 000000000..3f86ff2ae --- /dev/null +++ b/src/net/java/sip/communicator/impl/media/conference/BufferStreamAdapter.java @@ -0,0 +1,152 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.media.conference; + +import java.io.*; + +import javax.media.*; +import javax.media.protocol.*; + +/** + * Represents a base class for adapters of SourceStreams, usually + * ones reading data in arrays of bytes and not in Buffers, to + * SourceStreams reading data in Buffers. An example + * use is creating a PushBufferStream representation of a PushSourceStream. + * + * @author Lubomir Marinov + */ +public abstract class BufferStreamAdapter + implements SourceStream +{ + + /** + * The Format of this stream to be reported through the output + * Buffer this instance reads data into. + */ + private final Format format; + + /** + * The SourceStream being adapted by this instance. + */ + protected final T stream; + + /** + * Initializes a new BufferStreamAdapter which is to adapt a + * specific SourceStream into a SourceStream with + * a specific Format. + * + * @param stream + * @param format + */ + public BufferStreamAdapter(T stream, Format format) + { + this.stream = stream; + this.format = format; + } + + /* + * Implements SourceStream#endOfStream(). Delegates to the wrapped + * SourceStream. + */ + public boolean endOfStream() + { + return stream.endOfStream(); + } + + /* + * Implements SourceStream#getContentDescriptor(). Delegates to the wrapped + * SourceStream. + */ + public ContentDescriptor getContentDescriptor() + { + return stream.getContentDescriptor(); + } + + /* + * Implements SourceStream#getContentLength(). Delegates to the wrapped + * SourceStream. + */ + public long getContentLength() + { + return stream.getContentLength(); + } + + /* + * Implements Controls#getControl(String). Delegates to the wrapped + * SourceStream. + */ + public Object getControl(String controlType) + { + return stream.getControl(controlType); + } + + /* + * Implements Controls#getControls(). Delegates to the wrapped SourceStream. + */ + public Object[] getControls() + { + return stream.getControls(); + } + + /** + * Gets the Format of the data this stream provides. + * + * @return the Format of the data this stream provides + */ + public Format getFormat() + { + return format; + } + + /** + * Reads byte data from this stream into a specific Buffer + * which is to use a specific array of bytes for its data. + * + * @param buffer the Buffer to read byte data into from this + * instance + * @param bytes the array of bytes to read data into from this + * instance and to be set as the data of the specified + * buffer + * @throws IOException + */ + protected void read(Buffer buffer, byte[] bytes) + throws IOException + { + int offset = 0; + int numberOfBytesRead = read(bytes, offset, bytes.length); + + if (numberOfBytesRead > -1) + { + buffer.setData(bytes); + buffer.setOffset(offset); + buffer.setLength(numberOfBytesRead); + + Format format = getFormat(); + + if (format != null) + buffer.setFormat(format); + } + } + + /** + * Reads byte data from this stream into a specific array of + * bytes starting the storing at a specific offset and reading + * at most a specific number of bytes. + * + * @param buffer the array of bytes into which the data read + * from this stream is to be written + * @param offset the offset in the specified buffer at which + * writing data read from this stream should start + * @param length the maximum number of bytes to be written into the + * specified buffer + * @return the number of bytes read from this stream and written into the + * specified buffer + * @throws IOException + */ + protected abstract int read(byte[] buffer, int offset, int length) + throws IOException; +} diff --git a/src/net/java/sip/communicator/impl/media/conference/CachingPushBufferStream.java b/src/net/java/sip/communicator/impl/media/conference/CachingPushBufferStream.java new file mode 100644 index 000000000..c29324f59 --- /dev/null +++ b/src/net/java/sip/communicator/impl/media/conference/CachingPushBufferStream.java @@ -0,0 +1,329 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.media.conference; + +import java.io.*; + +import javax.media.*; +import javax.media.format.*; +import javax.media.protocol.*; + +import net.java.sip.communicator.impl.media.*; + +/** + * Enables reading from a PushBufferStream a certain maximum number + * of data units (e.g. bytes, shorts, ints) even if the + * PushBufferStream itself pushes a larger number of data units. + *

+ * An example use of this functionality is pacing a + * PushBufferStream which pushes more data units in a single step + * than a CaptureDevice. When these two undergo audio mixing, the + * different numbers of per-push data units will cause the + * PushBufferStream "play" itself faster than the + * CaptureDevice. + *

+ * + * @author Lubomir Marinov + */ +public class CachingPushBufferStream + implements PushBufferStream +{ + + /** + * The Buffer in which this instance stores the data it reads + * from the wrapped PushBufferStream and from which it reads in + * chunks later on when its #read(Buffer) method is called. + */ + private Buffer cache; + + /** + * The last IOException this stream has received from the + * #read(Buffer) method of the wrapped stream and to be thrown + * by this stream on the earliest call of its #read(Buffer) + * method. + */ + private IOException readException; + + /** + * The PushBufferStream being paced by this instance with + * respect to the maximum number of data units it provides in a single push. + */ + private final PushBufferStream stream; + + /** + * Initializes a new CachingPushBufferStream instance which is + * to pace the number of per-push data units a specific + * PushBufferStream provides. + * + * @param stream the PushBufferStream to be paced with respect + * to the number of per-push data units it provides + */ + public CachingPushBufferStream(PushBufferStream stream) + { + this.stream = stream; + } + + /* + * Implements SourceStream#endOfStream(). Delegates to the wrapped + * PushBufferStream when the cache of this instance is fully read; + * otherwise, returns false. + */ + public boolean endOfStream() + { + /* + * TODO If the cache is still not exhausted, don't report the end of + * this stream even if the wrapped stream has reached its end. + */ + return stream.endOfStream(); + } + + /* + * Implements SourceStream#getContentDescriptor(). Delegates to the wrapped + * PushBufferStream. + */ + public ContentDescriptor getContentDescriptor() + { + return stream.getContentDescriptor(); + } + + /* + * Implements SourceStream#getContentLength(). Delegates to the wrapped + * PushBufferStream. + */ + public long getContentLength() + { + return stream.getContentLength(); + } + + /* + * Implements Controls#getControl(String). Delegates to the wrapped + * PushBufferStream. + */ + public Object getControl(String controlType) + { + return stream.getControl(controlType); + } + + /* + * Implements Controls#getControls(). Delegates to the wrapped + * PushBufferStream. + */ + public Object[] getControls() + { + return stream.getControls(); + } + + /* + * Implements PushBufferStream#getFormat(). Delegates to the wrapped + * PushBufferStream. + */ + public Format getFormat() + { + return stream.getFormat(); + } + + /** + * Gets the object this instance uses for synchronization of the operations + * (such as reading from the wrapped stream into the cache of this instance + * and reading out of the cache into the Buffer provided to the + * #read(Buffer) method of this instance) it performs in + * various threads. + * + * @return the object this instance uses for synchronization of the + * operations it performs in various threads + */ + private Object getSyncRoot() + { + return this; + } + + /* + * Implements PushBufferStream#read(Buffer). If an IOException has been + * thrown by the wrapped stream when data was last read from it, re-throws + * it. If there is no such exception, reads from the cache of this instance. + */ + public void read(Buffer buffer) + throws IOException + { + Object syncRoot = getSyncRoot(); + + synchronized (syncRoot) + { + if (readException != null) + { + IOException ex = readException; + readException = null; + throw ex; + } + + if (cache != null) + { + try + { + read(cache, buffer); + } + catch (UnsupportedFormatException ufex) + { + IOException ioex = new IOException(); + ioex.initCause(ufex); + throw ioex; + } + + int cacheLength = cache.getLength(); + + if ((cacheLength <= 0) + || (cacheLength <= cache.getOffset()) + || (cache.getData() == null)) + { + cache = null; + syncRoot.notifyAll(); + } + } + } + } + + /** + * Reads data from a specific input Buffer (if such data is + * available) and writes the read data into a specific output + * Buffer. The input Buffer will be modified to + * reflect the number of read data units. If the output Buffer + * has allocated an array for storing the read data and the type of this + * array matches that of the input Buffer, it will be used and + * thus the output Buffer may control the maximum number of + * data units to be read into it. + * + * @param input the Buffer to read data from + * @param output the Buffer into which to write the data read + * from the specified input + * @throws IOException + * @throws UnsupportedFormatException + */ + private void read(Buffer input, Buffer output) + throws IOException, + UnsupportedFormatException + { + Object outputData = output.getData(); + + if (outputData != null) + { + Object inputData = input.getData(); + + if (inputData == null) + { + output.setFormat(input.getFormat()); + output.setLength(0); + return; + } + + Class dataType = outputData.getClass(); + + if (inputData.getClass().equals(dataType) + && dataType.equals(byte[].class)) + { + byte[] outputBytes = (byte[]) outputData; + int outputLength + = Math.min(input.getLength(), outputBytes.length); + + System.arraycopy( + (byte[]) inputData, + input.getOffset(), + outputBytes, + output.getOffset(), + outputLength); + + output.setData(outputBytes); + output.setFormat(input.getFormat()); + output.setLength(outputLength); + + input.setLength(input.getLength() - outputLength); + input.setOffset(input.getOffset() + outputLength); + return; + } + } + + output.copy(input); + + int outputLength = output.getLength(); + + input.setLength(input.getLength() - outputLength); + input.setOffset(input.getOffset() + outputLength); + } + + /* + * Implements PushBufferStream#setTransferHandler(BufferTransferHandler). + * Delegates to the wrapped PushBufferStream but wraps the specified + * BufferTransferHandler in order to intercept the calls to + * BufferTransferHandler#transferData(PushBufferStream) and read data from + * the wrapped PushBufferStream into the cache during the calls in question. + */ + public void setTransferHandler(BufferTransferHandler transferHandler) + { + stream.setTransferHandler( + (transferHandler == null) + ? null + : new StreamSubstituteBufferTransferHandler( + transferHandler, + stream, + this) + { + public void transferData(PushBufferStream stream) + { + if (CachingPushBufferStream.this.stream + == stream) + CachingPushBufferStream.this.transferData(); + + super.transferData(stream); + } + }); + } + + /** + * Reads data from the wrapped/input PushBufferStream into the cache of this + * stream if the cache is empty. If the cache is not empty, blocks the + * calling thread until the cache is emptied and data is read from the + * wrapped PushBufferStream into the cache. + */ + protected void transferData() + { + Object syncRoot = getSyncRoot(); + + synchronized (syncRoot) + { + boolean interrupted = false; + + try + { + while (cache != null) + try + { + syncRoot.wait(); + } + catch (InterruptedException ex) + { + interrupted = true; + } + } + finally + { + if (interrupted) + Thread.currentThread().interrupt(); + } + + cache = new Buffer(); + + try + { + stream.read(cache); + readException = null; + } + catch (IOException ex) + { + readException = ex; + } + } + } +} diff --git a/src/net/java/sip/communicator/impl/media/conference/PullBufferStreamAdapter.java b/src/net/java/sip/communicator/impl/media/conference/PullBufferStreamAdapter.java new file mode 100644 index 000000000..f167bfb6d --- /dev/null +++ b/src/net/java/sip/communicator/impl/media/conference/PullBufferStreamAdapter.java @@ -0,0 +1,120 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.media.conference; + +import java.io.*; + +import javax.media.*; +import javax.media.format.*; +import javax.media.protocol.*; + +/** + * Represents a PullBufferStream which reads its data from a + * specific PullSourceStream. + * + * @author Lubomir Marinov + */ +public class PullBufferStreamAdapter + extends BufferStreamAdapter + implements PullBufferStream +{ + + /** + * Initializes a new PullBufferStreamAdapter instance which + * reads its data from a specific PullSourceStream with a + * specific Format + * + * @param stream the PullSourceStream the new instance is to + * read its data from + * @param format the Format of the specified input + * stream and of the new instance + */ + public PullBufferStreamAdapter(PullSourceStream stream, Format format) + { + super(stream, format); + } + + /** + * Gets the frame size measured in bytes defined by a specific + * Format. + * + * @param format the Format to determine the frame size in + * bytes of + * @return the frame size measured in bytes defined by the specified + * Format + */ + private static int getFrameSizeInBytes(Format format) + { + AudioFormat audioFormat = (AudioFormat) format; + int frameSizeInBits = audioFormat.getFrameSizeInBits(); + + if (frameSizeInBits <= 0) + return + (audioFormat.getSampleSizeInBits() / 8) + * audioFormat.getChannels(); + return (frameSizeInBits <= 8) ? 1 : (frameSizeInBits / 8); + } + + /* + * Implements PullBufferStream#read(Buffer). Delegates to the wrapped + * PullSourceStream by either allocating a new byte[] buffer or using the + * existing one in the specified Buffer. + */ + public void read(Buffer buffer) + throws IOException + { + Object data = buffer.getData(); + byte[] bytes = null; + + if (data != null) + { + if (data instanceof byte[]) + bytes = (byte[]) data; + else if (data instanceof short[]) + { + short[] shorts = (short[]) data; + + bytes = new byte[2 * shorts.length]; + } + else if (data instanceof int[]) + { + int[] ints = (int[]) data; + + bytes = new byte[4 * ints.length]; + } + } + if (bytes == null) + { + int frameSizeInBytes = getFrameSizeInBytes(getFormat()); + + bytes + = new byte[ + 1024 * ((frameSizeInBytes <= 0) ? 4 : frameSizeInBytes)]; + } + + read(buffer, bytes); + } + + /* + * Implements BufferStreamAdapter#read(byte[], int, int). Delegates to the + * wrapped PullSourceStream. + */ + protected int read(byte[] buffer, int offset, int length) + throws IOException + { + return stream.read(buffer, offset, length); + } + + /* + * Implements PullBufferStream#willReadBlock(). Delegates to the wrapped + * PullSourceStream. + */ + public boolean willReadBlock() + { + return stream.willReadBlock(); + } +} diff --git a/src/net/java/sip/communicator/impl/media/conference/PushBufferStreamAdapter.java b/src/net/java/sip/communicator/impl/media/conference/PushBufferStreamAdapter.java new file mode 100644 index 000000000..a3369c547 --- /dev/null +++ b/src/net/java/sip/communicator/impl/media/conference/PushBufferStreamAdapter.java @@ -0,0 +1,75 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.media.conference; + +import java.io.*; + +import javax.media.*; +import javax.media.protocol.*; + +/** + * Represents a PushBufferStream which reads its data from a + * specific PushSourceStream. + * + * @author Lubomir Marinov + */ +public class PushBufferStreamAdapter + extends BufferStreamAdapter + implements PushBufferStream +{ + + /** + * Initializes a new PushBufferStreamAdapter instance which + * reads its data from a specific PushSourceStream with a + * specific Format + * + * @param stream the PushSourceStream the new instance is to + * read its data from + * @param format the Format of the specified input + * stream and of the new instance + */ + public PushBufferStreamAdapter(PushSourceStream stream, Format format) + { + super(stream, format); + } + + /* + * Implements PushBufferStream#read(Buffer). Delegates to the wrapped + * PushSourceStream by allocating a new byte[] buffer of size equal to + * PushSourceStream#getMinimumTransferSize(). + */ + public void read(Buffer buffer) + throws IOException + { + read(buffer, new byte[stream.getMinimumTransferSize()]); + } + + /* + * Implements BufferStreamAdapter#read(byte[], int, int). Delegates to the + * wrapped PushSourceStream. + */ + protected int read(byte[] buffer, int offset, int length) + throws IOException + { + return stream.read(buffer, offset, length); + } + + /* + * Implements PushBufferStream#setTransferHandler(BufferTransferHandler). + * Delegates to the wrapped PushSourceStream by translating the specified + * BufferTransferHandler to a SourceTransferHandler. + */ + public void setTransferHandler(final BufferTransferHandler transferHandler) + { + stream.setTransferHandler(new SourceTransferHandler() + { + public void transferData(PushSourceStream stream) { + transferHandler.transferData(PushBufferStreamAdapter.this); + } + }); + } +} diff --git a/src/net/java/sip/communicator/impl/media/conference/TranscodingDataSource.java b/src/net/java/sip/communicator/impl/media/conference/TranscodingDataSource.java new file mode 100644 index 000000000..2a73e65d7 --- /dev/null +++ b/src/net/java/sip/communicator/impl/media/conference/TranscodingDataSource.java @@ -0,0 +1,280 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.media.conference; + +import java.io.*; +import java.lang.reflect.*; + +import javax.media.*; +import javax.media.control.*; +import javax.media.format.*; +import javax.media.protocol.*; + +import net.java.sip.communicator.impl.media.*; + +/** + * Represents a DataSource which transcodes the tracks of a + * specific input DataSource into a specific output + * Format. The transcoding is attempted only for tracks which + * actually support it for the specified output Format. + * + * @author Lubomir Marinov + */ +public class TranscodingDataSource + extends DataSource +{ + + /** + * The DataSource which has its tracks transcoded by this + * instance. + */ + private final DataSource inputDataSource; + + /** + * The DataSource which contains the transcoded tracks of + * inputDataSource and which is wrapped by this instance. It is + * the output of transcodingProcessor. + */ + private DataSource outputDataSource; + + /** + * The Format in which the tracks of + * inputDataSource are transcoded. + */ + private final Format outputFormat; + + /** + * The Processor which carries out the actual transcoding of + * the tracks of inputDataSource. + */ + private Processor transcodingProcessor; + + /** + * Initializes a new TranscodingDataSource instance to + * transcode the tracks of a specific DataSource into a + * specific output Format. + * + * @param inputDataSource the DataSource which is to have its + * tracks transcoded in a specific outptu Format + * @param outputFormat the Format in which the new instance is + * to transcode the tracks of inputDataSource + */ + public TranscodingDataSource( + DataSource inputDataSource, + Format outputFormat) + { + super(inputDataSource.getLocator()); + + this.inputDataSource = inputDataSource; + this.outputFormat = outputFormat; + } + + /* + * Implements DataSource#connect(). Sets up the very transcoding process and + * just does not start it i.e. creates a Processor on the inputDataSource, + * sets outputFormat on its tracks (which support a Format compatible with + * outputFormat) and connects to its output DataSource. + */ + public void connect() + throws IOException + { + if (outputDataSource != null) + return; + + Processor processor; + + try + { + processor = Manager.createProcessor(inputDataSource); + } + catch (NoProcessorException npex) + { + IOException ioex = new IOException(); + ioex.initCause(npex); + throw ioex; + } + + ProcessorUtility processorUtility = new ProcessorUtility(); + + if (!processorUtility.waitForState(processor, Processor.Configured)) + throw new IOException("Couldn't configure transcoding processor."); + + TrackControl[] trackControls = processor.getTrackControls(); + + if (trackControls != null) + for (TrackControl trackControl : trackControls) + { + Format trackFormat = trackControl.getFormat(); + + /* + * XXX We only care about AudioFormat here and we assume + * outputFormat is of such type because it is in our current and + * only use case of TranscodingDataSource + */ + if ((trackFormat instanceof AudioFormat) + && !trackFormat.matches(outputFormat)) + { + Format[] supportedTrackFormats + = trackControl.getSupportedFormats(); + + if (supportedTrackFormats != null) + for (Format supportedTrackFormat + : supportedTrackFormats) + if (supportedTrackFormat.matches(outputFormat)) + { + Format intersectionFormat + = supportedTrackFormat.intersects( + outputFormat); + + if (intersectionFormat != null) + { + trackControl.setFormat(intersectionFormat); + break; + } + } + } + } + + if (!processorUtility.waitForState(processor, Processor.Realized)) + throw new IOException("Couldn't realize transcoding processor."); + + DataSource outputDataSource = processor.getDataOutput(); + outputDataSource.connect(); + + transcodingProcessor = processor; + this.outputDataSource = outputDataSource; + } + + /* + * Implements DataSource#disconnect(). Stops and undoes the whole setup of + * the very transcoding process i.e. disconnects from the output DataSource + * of the transcodingProcessor and disposes of the transcodingProcessor. + */ + public void disconnect() + { + if (outputDataSource == null) + return; + + try + { + stop(); + } + catch (IOException ioex) + { + throw new UndeclaredThrowableException(ioex); + } + + outputDataSource.disconnect(); + + transcodingProcessor.deallocate(); + transcodingProcessor.close(); + transcodingProcessor = null; + + outputDataSource = null; + } + + /* + * Implements DataSource#getContentType(). Delegates to the actual output of + * the transcoding. + */ + public String getContentType() + { + return + (outputDataSource == null) + ? null + : outputDataSource.getContentType(); + } + + /* + * Implements DataSource#getControl(String). Delegates to the actual output + * of the transcoding. + */ + public Object getControl(String controlType) + { + /* + * The Javadoc of DataSource#getControl(String) says it's an error to + * call the method without being connected and by that time we should + * have the outputDataSource. + */ + return outputDataSource.getControl(controlType); + } + + /* + * Implements DataSource#getControls(). Delegates to the actual output of + * the transcoding. + */ + public Object[] getControls() + { + return + (outputDataSource == null) + ? new Object[0] + : outputDataSource.getControls(); + } + + /* + * Implements DataSource#getDuration(). Delegates to the actual output of + * the transcoding. + */ + public Time getDuration() + { + return + (outputDataSource == null) + ? DURATION_UNKNOWN + : outputDataSource.getDuration(); + } + + /** + * Gets the output streams that this instance provides. Some of them may be + * the result of transcoding the tracks of the input DataSource + * of this instance in the output Format of this instance. + * + * @return an array of SourceStreams which represents the + * collection of output streams that this instance provides + */ + public SourceStream[] getStreams() + { + if (outputDataSource instanceof PushBufferDataSource) + return ((PushBufferDataSource) outputDataSource).getStreams(); + if (outputDataSource instanceof PullBufferDataSource) + return ((PullBufferDataSource) outputDataSource).getStreams(); + if (outputDataSource instanceof PushDataSource) + return ((PushDataSource) outputDataSource).getStreams(); + if (outputDataSource instanceof PullDataSource) + return ((PullDataSource) outputDataSource).getStreams(); + return new SourceStream[0]; + } + + /* + * Implements DataSource#start(). Starts the actual transcoding process + * already set up with #connect(). + */ + public void start() + throws IOException + { + /* + * The Javadoc of DataSource#start() says it's an error to call the + * method without being connected and by that time we should have the + * outputDataSource. + */ + outputDataSource.start(); + transcodingProcessor.start(); + } + + /* + * Implements DataSource#stop(). Stops the actual transcoding process if it + * has already been set up with #connect(). + */ + public void stop() + throws IOException + { + if (outputDataSource != null) + { + transcodingProcessor.stop(); + outputDataSource.stop(); + } + } +}