Create Custom Sources and Sinks
Note
This API is not available for JavaScript. Instead, you can provide any HTML5 MediaStream to the LocalMedia
constructor as the audio or video parameter to be used as a custom source. Custom sinks in JavaScript are not supported.
LiveSwitch uses the concepts of sources and sinks:
- A source captures data sent to another client.
- A sink renders data received from another client.
LiveSwitch provides several implementations of sources and sinks for common use cases. However, you can implement your own sources and sinks for specific use cases that LiveSwitch doesn't support by default, such as streaming received video to other devices.
Audio Formats
Each AudioSource
and AudioSink
instance has an associated AudioFormat
instance.
An audio format consists of a clock rate, in Hz, and the number of audio channels. It indicates the following:
- For a source: the format of the audio raised by the source as output.
- For a sink: the format of the audio processed by the sink as input.
You need to specify AudioFormat
instances for your sources and sinks. For your convenience, LiveSwitch provides a number of pre-defined formats that you can use directly.
The following code example creates several AudioFormat
instances:
var opusFormat = new FM.LiveSwitch.Opus.Format();
var pcmaFormat = new FM.LiveSwitch.Pcma.Format();
var pcmuFormat = new FM.LiveSwitch.Pcmu.Format();
Another commonly used format is the Pcm.Format
class. This format specifies a generic PCM format with a custom clock rate and audio channel count.
The following code example creates a 48,000
Hz, 2
channel audio format instance.
For the complete list of pre-defined audio formats, refer to the Client API Reference.
Video Formats
Similar to the audio formats, each VideoSource
and VideoSink
instance has an associated VideoFormat
instance. A video format consists of a clock rate and information about the color space of the format. For your convenience, LiveSwitch provides a number of pre-defined formats that you can use directly.
The following code example creates the two most common video formats: RGB and I420.
var rgbFormat = FM.LiveSwitch.VideoFormat.Rgb;
var i420Format = FM.LiveSwitch.VideoFormat.I420;
Custom Sources
To create a custom audio or video source, first create a class that extends either the AudioSource
or VideoSource
class. Neither of these classes have a default constructor. They require you to specify either an AudioFormat
or an VideoFormat
instance. Most custom sources are designed for a specific output format. It's common to create a default constructor that invokes the base constructor with a pre-defined format. The following code demonstrates this.
public class CustomAudioSource : FM.LiveSwitch.AudioSource
{
public CustomAudioSource()
: base(new FM.LiveSwitch.Pcm.Format(48000, 2))
{
}
}
public class CustomVideoSource : FM.LiveSwitch.VideoSource
{
public CustomVideoSource()
: base(FM.LiveSwitch.VideoFormat.Rgb)
{
}
}
We recommend extending the CameraSourceBase
or ScreenSourceBase
classes instead of the VideoSource
class. Using these classes allows the pipeline to optimize its default configuration for those specific use cases as well as signal the media type to other clients.
- Extending
CameraSourceBase
requires an additional constructor parameter of typeVideoConfig
to indicate the target configuration:size
andframe-rate
. TheDoStart
implementation must then set theConfig
property to the actual selected camera configuration. - Extending
ScreenSourceBase
requires an additional constructor parameter of typeScreenConfig
to indicate the target configuration:origin
,region
, andframe-rate
. TheDoStart
implementation must then set theConfig
property of the actual selected screen configuration.
Next, override the Label
property. This is an accessor that returns a string that identifies the type of source. The value you provide here is only for diagnostic purposes and doesn't affect the output of an audio or video source.
public class CustomAudioSource : FM.LiveSwitch.AudioSource
{
public override string Label => "CustomAudioSource";
}
public class CustomVideoSource : FM.LiveSwitch.VideoSource
{
public override string Label => "CustomVideoSource";
}
Finally, you must implement the DoStart
and DoStop
methods. Usually, these methods follow one of two patterns:
- Manage an event handler on a interface that captures audio and video data
- Manage a separate thread that runs in the background, which generates audio and video data.
With both patterns, the source must invoke the RaiseFrame
method when data is available. RaiseFrame
is a protected method that signals to components in the media stack that new data is available.
Note that the DoStart
and DoStop
methods are asynchronous and return an FM.LiveSwitch.Future
. For the sake of simplicity, these examples are synchronous and resolve the promise immediately. In practice, your implementation is likely to be more complex.
Capture Audio
The following code examples show how to capture audio using the event-based pattern. These code examples use a fictional AudioCaptureObject
class created for demonstration purposes.
Raise Audio Frame
To capture audio, first create an instance of the AudioCaptureObject
, and then add an event handler that is raised whenever new audio data is available. The event handler has the following three parameters:
data
: A byte array that contains raw audio data over a period of time.duration
: The time in milliseconds that thedata
parameter represents. You must calculate the duration based on your implementation. If the audio source is raising uncompressed (PCM) audio data, you can infer theduration
directly from the length of data and the clock-rate and channel-count of the output's audio format. You can useSoundUtility
, which includes a number of static helper methods, to perform this calculation. For example:var duration = SoundUtility.CalculateDuration(data.Length, OutputFormat.Config);
systemTimestamp
: A timestamp measured in ticks. 10,000 ticks are equivalent to 1 millisecond. This timestamp must come from the system clock used by theVideoSource
object that theAudioSource
object is going to synchronize with. To synchronize audio with a video source, like for lip syncing, set theAudioSource
object'sOutputSynchronizable
property totrue
in the constructor.
Note
By default, the OutputSynchronizable
property of a VideoSource
object is set to true
. VideoSource
uses ManagedStopwatch.GetTimestamp()
to set SystemTimestamp
values automatically on raised VideoFrame
instances.
Depending on the platform, ManagedStopwatch
gets timestamps from the following places:
- C#: System.Diagnostics.Stopwatch.GetTimestamp(). Uses Stopwatch.Frequency to convert the timestamp to normalized ticks.
- Android: System.nanoTime(). Converted to ticks where 1 tick is 100 nanoseconds.
- iOS: mach_absolute_time(). Converted to ticks using this function where 1 tick is 100 nanoseconds.
You can raise an audio frame with these three parameters as follows:
Wrap the raw audio data in an instance of
FM.LiveSwitch.DataBuffer
.Important
LiveSwitch only supports signed 16-bit (short) and little-endian for raising uncompressed (PCM) audio data. Other PCM formats, like 32-bit (floating point) and big-endian, must be converted to signed 16-bit (short) and little-endian in the source.
Wrap the data buffer in an instance of
FM.LiveSwitch.AudioBuffer
, which also requires you to specify theFM.LiveSwitch.AudioFormat
of the audio data. You can use theOutputFormat
property of your audio source to retrieve this.Wrap the audio buffer in an instance of
FM.LiveSwitch.AudioFrame
and provide the audio duration.Set the
AudioFrame
's SystemTimestamp.Invoke
RaiseFrame
on this newAudioFrame
instance.
public class CustomAudioSource : FM.LiveSwitch.AudioSource
{
private AudioCaptureObject _Capture;
protected override FM.LiveSwitch.Future<object> DoStart()
{
var promise = new FM.LiveSwitch.Promise<object>();
_Capture = new AudioCaptureObject();
_Capture.AudioDataAvailable += (int duration, byte[] data, long systemTimestamp) =>
{
// This sets the `littleEndian` flag to true.
var dataBuffer = FM.LiveSwitch.DataBuffer.Wrap(data, true);
var audioBuffer = new FM.LiveSwitch.AudioBuffer(dataBuffer, this.OutputFormat);
var audioFrame = new FM.LiveSwitch.AudioFrame(duration, audioBuffer);
audioFrame.SystemTimestamp = systemTimestamp;
this.RaiseFrame(audioFrame);
});
promise.Resolve(null);
return promise;
}
}
Stop Audio Source
To stop an audio source instance, you can either destroy any capture interface you were using or remove any event handlers.
public class CustomAudioSource : FM.LiveSwitch.AudioSource
{
protected override Future<object> DoStop()
{
var promise = new FM.LiveSwitch.Promise<object>();
_Capture.Destroy();
_Capture = null;
promise.Resolve(null);
return promise;
}
}
Capture Video
The following examples demonstrate how to capture video using the event-based pattern and stop video to release resources. A fictional VideoCaptureObject
class is used for demo purpose.
Raise Video Frame
You need to specify the width and height of the video frames.
To raise a video frame:
- Wrap the raw video data in an instance of
FM.LiveSwitch.DataBuffer
. - Wrap the data buffer in an instance of
FM.LiveSwitch.VideoBuffer
. It requires you to specify theFM.LiveSwitch.VideoFormat
of the data as well as the width and height of the video. You can use theOutputFormat
property of your video source. - Set the stride values describing the video data in the data buffer.
- Wrap the video buffer in an instance of
FM.LiveSwitch.VideoFrame
and invokeRaiseFrame
.
public class CustomVideoSource : FM.LiveSwitch.VideoSource
{
private VideoCaptureObject _Capture;
protected override FM.LiveSwitch.Future<object> DoStart()
{
var promise = new FM.LiveSwitch.Promise<object>();
_Capture = new VideoCaptureObject();
_Capture.VideoDataAvailable += (int width, int height, byte[] data) =>
{
var dataBuffer = FM.LiveSwitch.DataBuffer.Wrap(data);
var videoBuffer = new FM.LiveSwitch.VideoBuffer(width, height, dataBuffer, this.OutputFormat);
videoBuffer.setStrides(new int[] { yPlaneStride, uPlaneStride, vPlaneStride });
var videoFrame = new FM.LiveSwitch.VideoFrame(videoBuffer);
this.RaiseFrame(videoFrame);
});
promise.Resolve(null);
return promise;
}
}
Stop Video Source
To stop a video source, simply release or destroy any resources you were using.
public class CustomVideoSource : FM.LiveSwitch.VideoSource
{
protected override Future<object> DoStop()
{
var promise = new FM.LiveSwitch.Promise<object>();
_Capture.Destroy();
_Capture = null;
promise.Resolve(null);
return promise;
}
}
Raise Frames
You should raise audio frames as soon as the audio frames are accessed from the underlying device or API. LiveSwitch automatically handles any gap in the audio streams.
You should raise video frames as soon as the video frames are accessed from the underlying device or API. LiveSwitch automatically handles missed video frames due to congestion or device load. If you implement your own queue of video frames, we recommend to discard rather than increasing the queue length. A frame-rate reduction is generally preferred to a delivery delay.
Custom Sinks
Like you did when creating a custom source, to create a custom audio or video sink, first extends either the AudioSink
or VideoSink
class. The sink takes on the output format of any source or pipe that is attached to it. You only need to specify an AudioFormat
or VideoFormat
if you need to restrict the input format.
The following code is a simple example of how to create a custom sink.
public class CustomAudioSink : FM.LiveSwitch.AudioSink
{
public CustomAudioSink()
: base(new FM.LiveSwitch.Pcm.Format(48000, 2))
{
}
}
public class CustomVideoSink : FM.LiveSwitch.VideoSink
{
public CustomVideoSink()
: base(FM.LiveSwitch.VideoFormat.RGB)
{
}
}
Sinks have a Label
property that is used for diagnosing. It doesn't affect what goes into your sinks.
public class CustomAudioSink : FM.LiveSwitch.AudioSink
{
public override string Label => "CustomAudioSink";
}
public class CustomVideoSink : FM.LiveSwitch.VideoSink
{
public override string Label => "CustomVideoSink";
}
Unlike source, the implementation for sinks has no DoStart
or DoStop
methods because sinks don't follow a "start/stop" pattern. Instead, whenever an audio or video frame is available, the sink invokes its DoProcessFrame
method. When a sink is instantiated, it is assumed to be ready to receive frames.
Tip
Sinks can lazy-initialize, such as initialize themselves in the first DoProcessFrame
invocation as opposed to in the constructor.
The last method that sinks must implement is DoDestroy
, which cleans up any resources that are still in use. The DoProcessFrame
and DoDestroy
methods for a sink are synchronous, and don't return an FM.LiveSwitch.Promise
.
Note
LiveSwitch guarantees that DoProcessFrame
is only called once at a time and is thread-safe. LiveSwitch also guarantees that DoDestroy
is never called concurrently with DoProcessFrame
.
Render Audio
To demonstrate how to play received audio data, the example code below uses a fictional AudioRenderObject
class and abstracts away many of the details of audio playback. In your implementation, you must deal with the upsampling and downsampling of audio.
There are many properties that are accessible from the AudioFrame
and AudioBuffer
classes. This example focuses on retrieving the duration
and data
properties. Assume the AudioRenderObject
has a PlayAudio
method that takes a duration
parameter and a data
parameter. You must retrieve these values from either the audio buffer or the audio frame:
- Retrieve the duration of the audio frame by accessing the
Duration
property of theAudioFrame
parameter. - Retrieve the
DataBuffer
property ofAudiobuffer
, and then retrieve the raw audio data through theData
property of thisDataBuffer
instance. - Pass these values into the
AudioRenderObject
(or any interface you are using for this sink.)
public class CustomAudioSink : FM.LiveSwitch.AudioSink
{
private AudioRenderObject _Render = new AudioRenderObject();
public override void DoProcessFrame(FM.LiveSwitch.AudioFrame frame, FM.LiveSwitch.AudioBuffer buffer)
{
var duration = frame.Duration;
var dataBuffer = buffer.DataBuffer;
var data = dataBuffer.Data;
_Render.PlayAudio(duration, data);
}
}
Render Video
Rendering video is similar to rendering audio. A fictional VideoRenderObject
class is used for demo purpose.
The example below demonstrates how to retrieve the width and height of video data in the DoProcessFrame
method, and the raw video data with the following steps:
- Retrieve the
Width
andHeight
properties of the video buffer parameter. - Retrieve the
DataBuffer
property of the same parameter, and then retrieve the raw video data through theData
property of theDataBuffer
instance. - Pass these values into the
VideoRenderObject
(or whatever interface you are using for this sink.) - Finally, Implement
DoDestroy
to release any resource you used.
public class CustomVideoSink : FM.LiveSwitch.VideoSink
{
private VideoRenderObject _Render = new VideoRenderObject();
public override void DoProcessFrame(FM.LiveSwitch.VideoFrame frame, FM.LiveSwitch.VideoBuffer buffer)
{
var width = buffer.Width;
var height = buffer.Height;
var dataBuffer = buffer.DataBuffer;
var data = dataBuffer.Data;
_Render.PlayVideo(width, height, data);
}
public override void DoDestroy()
{
_Render.Destroy();
_Render = null;
}
}