Represents the first take at implementing autio mixing for the purposes of conferencing. The functionality in question is still not being used and is to be employed later when the other conferencing bits are added.

cusax-fix
Lyubomir Marinov 17 years ago
parent ba1a912b02
commit 8f4466385c

@ -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 <code>PushBufferDataSource</code> which is also a
* <code>CaptureDevice</code> through delegation to a specific
* <code>CaptureDevice</code>.
*
* @author Lubomir Marinov
*/
public abstract class CaptureDeviceDelegatePushBufferDataSource
extends PushBufferDataSource
implements CaptureDevice
{
/**
* The <code>CaptureDevice</code> this instance delegates to in order to
* implement its <code>CaptureDevice</code> functionality.
*/
private final CaptureDevice captureDevice;
/**
* Initializes a new <code>CaptureDeviceDelegatePushBufferDataSource</code>
* instance which delegates to a specific <code>CaptureDevice</code> in
* order to implement its <code>CaptureDevice</code> functionality.
*
* @param captureDevice the <code>CaptureDevice</code> the new instance is
* to delegate to in order to provide its
* <code>CaptureDevice</code> 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];
}
}

@ -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 <tt>BufferTransferHandler</tt> wrapper which doesn't
* expose a wrapped <tt>PushBufferStream</tt> 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 <tt>BufferTransferHandler</tt> which receives the
* actual events from the wrapped <tt>PushBufferStream</tt>.
*/
private final BufferTransferHandler transferHandler;
/**
* Initializes a new <tt>MuteBufferTransferHandler</tt> instance
* which is to overwrite the source <tt>PushBufferStream</tt> of a
* specific <tt>BufferTransferHandler</tt>.
*
* @param transferHandler the <tt>BufferTransferHandler</tt> the new
* instance is to overwrite the source
* <tt>PushBufferStream</tt> 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));
}
}
}

@ -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 <tt>BufferTransferHandler</tt> wrapper which doesn't
* expose a <tt>PushBufferStream</tt> but rather a specific substitute in order
* to give full control to the {@link PushBufferStream#read(Buffer)} method of
* the substitute.
* <p>
* The purpose is achieved in <code>#transferData(PushBufferStream)</code>
* where the method argument <code>stream</code> is ignored and the substitute
* is used instead.
* </p>
*
* @author Lubomir Marinov
*/
public class StreamSubstituteBufferTransferHandler
implements BufferTransferHandler
{
/**
* The <code>PushBufferStream</code> to be overridden for
* <code>transferHandler</code> with the <code>substitute</code> of this
* instance.
*/
private final PushBufferStream stream;
/**
* The <code>PushBufferStream</code> to override the <code>stream</code> of
* this instance for <code>transferHandler</code>.
*/
private final PushBufferStream substitute;
/**
* The wrapped <tt>BufferTransferHandler</tt> which receives the
* actual events from the wrapped <tt>PushBufferStream</tt>.
*/
private final BufferTransferHandler transferHandler;
/**
* Initializes a new <tt>StreamSubstituteBufferTransferHandler</tt> instance
* which is to overwrite the source <tt>PushBufferStream</tt> of a specific
* <tt>BufferTransferHandler</tt>.
*
* @param transferHandler the <tt>BufferTransferHandler</tt> the new
* instance is to overwrite the source <tt>PushBufferStream</tt>
* of
* @param stream the <code>PushBufferStream</code> to be overridden for the
* specified <code>transferHandler</code> with the specified
* <code>substitute</code>
* @param substitute the <code>PushBufferStream</code> to override the
* specified <code>stream</code> for the specified
* <code>transferHandler</code>
*/
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);
}
}

@ -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 <code>PushBufferDataSource</code> which provides a single
* <code>PushBufferStream</code> containing the result of the audio mixing of
* <code>DataSource</code>s.
*
* @author Lubomir Marinov
*/
public class AudioMixingPushBufferDataSource
extends PushBufferDataSource
implements CaptureDevice
{
/**
* The <code>AudioMixer</code> performing the audio mixing, managing the
* input <code>DataSource</code>s and pushing the data of this output
* <code>PushBufferDataSource</code>.
*/
private final AudioMixer audioMixer;
/**
* The indicator which determines whether this <code>DataSource</code> is
* connected.
*/
private boolean connected;
/**
* The one and only <code>PushBufferStream</code> this
* <code>PushBufferDataSource</code> provides to its clients and containing
* the result of the audio mixing performed by <code>audioMixer</code>.
*/
private AudioMixingPushBufferStream outputStream;
/**
* The indicator which determines whether this <code>DataSource</code> is
* started.
*/
private boolean started;
/**
* Initializes a new <code>AudioMixingPushBufferDataSource</code> instance
* which gives access to the result of the audio mixing performed by a
* specific <code>AudioMixer</code>.
*
* @param audioMixer the <code>AudioMixer</code> performing audio mixing,
* managing the input <code>DataSource</code>s and pushing the
* data of the new output <code>PushBufferDataSource</code>
*/
public AudioMixingPushBufferDataSource(AudioMixer audioMixer)
{
this.audioMixer = audioMixer;
}
/**
* Adds a new input <code>DataSource</code> to be mixed by the associated
* <code>AudioMixer</code> of this instance and to not have its audio
* contributions included in the mixing output represented by this
* <code>DataSource</code>.
*
* @param inputDataSource a <code>DataSource</code> to be added for mixing
* to the <code>AudioMixer</code> associate with this instance
* and to not have its audio contributions included in the mixing
* output represented by this <code>DataSource</code>
*/
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;
}
}
}

@ -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 <code>PushBufferStream</code> containing the result of the audio
* mixing of <code>DataSource</code>s.
*
* @author Lubomir Marinov
*/
public class AudioMixingPushBufferStream
implements PushBufferStream
{
/**
* The <code>AudioMixer.AudioMixerPushBufferStream</code> which reads data
* from the input <code>DataSource</code>s and pushes it to this instance to
* be mixed.
*/
private final AudioMixer.AudioMixerPushBufferStream audioMixerStream;
/**
* The <code>AudioMixingPushBufferDataSource</code> which created and owns
* this instance and defines the input data which is to not be mixed in the
* output of this <code>PushBufferStream</code>.
*/
private final AudioMixingPushBufferDataSource dataSource;
/**
* The collection of input audio samples still not mixed and read through
* this <code>AudioMixingPushBufferStream</code>.
*/
private int[][] inputSamples;
/**
* The maximum number of per-stream audio samples available through
* <code>inputSamples</code>.
*/
private int maxInputSampleCount;
/**
* The <code>BufferTransferHandler</code> through which this
* <code>PushBufferStream</code> notifies its clients that new data is
* available for reading.
*/
private BufferTransferHandler transferHandler;
/**
* Initializes a new <code>AudioMixingPushBufferStream</code> mixing the
* input data of a specific
* <code>AudioMixer.AudioMixerPushBufferStream</code> and excluding from the
* mix the audio contributions of a specific
* <code>AudioMixingPushBufferDataSource</code>.
*
* @param audioMixerStream the
* <code>AudioMixer.AudioMixerPushBufferStream</code> reading
* data from input <code>DataSource</code>s and to push it to the
* new <code>AudioMixingPushBufferStream</code>
* @param dataSource the <code>AudioMixingPushBufferDataSource</code> 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 <code>AudioMixingPushBufferDataSource</code> which created and
* owns this instance and defines the input data which is to not be mixed in
* the output of this <code>PushBufferStream</code>.
*
* @return the <code>AudioMixingPushBufferDataSource</code> which created
* and owns this instance and defines the input data which is to not
* be mixed in the output of this <code>PushBufferStream</code>
*/
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
* <code>AudioFormat</code>.
*
* @param outputFormat the <code>AudioFormat</code> of which to get the
* maximum possible value for an audio sample
* @return the maximum possible value for an audio sample of the specified
* <code>AudioFormat</code>
* @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
* <code>AudioFormat</code>.
*
* @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 <code>AudioFormat</code> 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 <code>inputSamples</code>
*/
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 <code>output</code> 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 <code>output</code> 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);
}
}

@ -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 <code>SourceStream</code>s, usually
* ones reading data in arrays of bytes and not in <code>Buffer</code>s, to
* <code>SourceStream</code>s reading data in <code>Buffer</code>s. An example
* use is creating a PushBufferStream representation of a PushSourceStream.
*
* @author Lubomir Marinov
*/
public abstract class BufferStreamAdapter<T extends SourceStream>
implements SourceStream
{
/**
* The <code>Format</code> of this stream to be reported through the output
* <code>Buffer</code> this instance reads data into.
*/
private final Format format;
/**
* The <code>SourceStream</code> being adapted by this instance.
*/
protected final T stream;
/**
* Initializes a new <code>BufferStreamAdapter</code> which is to adapt a
* specific <code>SourceStream</code> into a <code>SourceStream</code> with
* a specific <code>Format</code>.
*
* @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 <code>Format</code> of the data this stream provides.
*
* @return the <code>Format</code> of the data this stream provides
*/
public Format getFormat()
{
return format;
}
/**
* Reads byte data from this stream into a specific <code>Buffer</code>
* which is to use a specific array of bytes for its data.
*
* @param buffer the <code>Buffer</code> to read byte data into from this
* instance
* @param bytes the array of <code>byte</code>s to read data into from this
* instance and to be set as the data of the specified
* <code>buffer</code>
* @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
* <code>byte</code>s starting the storing at a specific offset and reading
* at most a specific number of bytes.
*
* @param buffer the array of <code>byte</code>s into which the data read
* from this stream is to be written
* @param offset the offset in the specified <code>buffer</code> at which
* writing data read from this stream should start
* @param length the maximum number of bytes to be written into the
* specified <code>buffer</code>
* @return the number of bytes read from this stream and written into the
* specified <code>buffer</code>
* @throws IOException
*/
protected abstract int read(byte[] buffer, int offset, int length)
throws IOException;
}

@ -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 <code>PushBufferStream</code> a certain maximum number
* of data units (e.g. bytes, shorts, ints) even if the
* <code>PushBufferStream</code> itself pushes a larger number of data units.
* <p>
* An example use of this functionality is pacing a
* <code>PushBufferStream</code> which pushes more data units in a single step
* than a <code>CaptureDevice</code>. When these two undergo audio mixing, the
* different numbers of per-push data units will cause the
* <code>PushBufferStream</code> "play" itself faster than the
* <code>CaptureDevice</code>.
* </p>
*
* @author Lubomir Marinov
*/
public class CachingPushBufferStream
implements PushBufferStream
{
/**
* The <code>Buffer</code> in which this instance stores the data it reads
* from the wrapped <code>PushBufferStream</code> and from which it reads in
* chunks later on when its <code>#read(Buffer)</code> method is called.
*/
private Buffer cache;
/**
* The last <code>IOException</code> this stream has received from the
* <code>#read(Buffer)</code> method of the wrapped stream and to be thrown
* by this stream on the earliest call of its <code>#read(Buffer)</code>
* method.
*/
private IOException readException;
/**
* The <code>PushBufferStream</code> 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 <code>CachingPushBufferStream</code> instance which is
* to pace the number of per-push data units a specific
* <code>PushBufferStream</code> provides.
*
* @param stream the <code>PushBufferStream</code> 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 <code>Buffer</code> provided to the
* <code>#read(Buffer)</code> 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 <code>Buffer</code> (if such data is
* available) and writes the read data into a specific output
* <code>Buffer</code>. The input <code>Buffer</code> will be modified to
* reflect the number of read data units. If the output <code>Buffer</code>
* has allocated an array for storing the read data and the type of this
* array matches that of the input <code>Buffer</code>, it will be used and
* thus the output <code>Buffer</code> may control the maximum number of
* data units to be read into it.
*
* @param input the <code>Buffer</code> to read data from
* @param output the <code>Buffer</code> into which to write the data read
* from the specified <code>input</code>
* @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;
}
}
}
}

@ -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 <code>PullBufferStream</code> which reads its data from a
* specific <code>PullSourceStream</code>.
*
* @author Lubomir Marinov
*/
public class PullBufferStreamAdapter
extends BufferStreamAdapter<PullSourceStream>
implements PullBufferStream
{
/**
* Initializes a new <code>PullBufferStreamAdapter</code> instance which
* reads its data from a specific <code>PullSourceStream</code> with a
* specific <code>Format</code>
*
* @param stream the <code>PullSourceStream</code> the new instance is to
* read its data from
* @param format the <code>Format</code> of the specified input
* <code>stream</code> 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
* <code>Format</code>.
*
* @param format the <code>Format</code> to determine the frame size in
* bytes of
* @return the frame size measured in bytes defined by the specified
* <code>Format</code>
*/
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();
}
}

@ -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 <code>PushBufferStream</code> which reads its data from a
* specific <code>PushSourceStream</code>.
*
* @author Lubomir Marinov
*/
public class PushBufferStreamAdapter
extends BufferStreamAdapter<PushSourceStream>
implements PushBufferStream
{
/**
* Initializes a new <code>PushBufferStreamAdapter</code> instance which
* reads its data from a specific <code>PushSourceStream</code> with a
* specific <code>Format</code>
*
* @param stream the <code>PushSourceStream</code> the new instance is to
* read its data from
* @param format the <code>Format</code> of the specified input
* <code>stream</code> 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);
}
});
}
}

@ -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 <code>DataSource</code> which transcodes the tracks of a
* specific input <code>DataSource</code> into a specific output
* <code>Format</code>. The transcoding is attempted only for tracks which
* actually support it for the specified output <code>Format</code>.
*
* @author Lubomir Marinov
*/
public class TranscodingDataSource
extends DataSource
{
/**
* The <code>DataSource</code> which has its tracks transcoded by this
* instance.
*/
private final DataSource inputDataSource;
/**
* The <code>DataSource</code> which contains the transcoded tracks of
* <code>inputDataSource</code> and which is wrapped by this instance. It is
* the output of <code>transcodingProcessor</code>.
*/
private DataSource outputDataSource;
/**
* The <code>Format</code> in which the tracks of
* <code>inputDataSource</code> are transcoded.
*/
private final Format outputFormat;
/**
* The <code>Processor</code> which carries out the actual transcoding of
* the tracks of <code>inputDataSource</code>.
*/
private Processor transcodingProcessor;
/**
* Initializes a new <code>TranscodingDataSource</code> instance to
* transcode the tracks of a specific <code>DataSource</code> into a
* specific output <code>Format</code>.
*
* @param inputDataSource the <code>DataSource</code> which is to have its
* tracks transcoded in a specific outptu <code>Format</code>
* @param outputFormat the <code>Format</code> in which the new instance is
* to transcode the tracks of <code>inputDataSource</code>
*/
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 <code>DataSource</code>
* of this instance in the output <code>Format</code> of this instance.
*
* @return an array of <code>SourceStream</code>s 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();
}
}
}
Loading…
Cancel
Save