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
CameraSourceBaserequires an additional constructor parameter of typeVideoConfigto indicate the target configuration:sizeandframe-rate. TheDoStartimplementation must then set theConfigproperty to the actual selected camera configuration. - Extending
ScreenSourceBaserequires an additional constructor parameter of typeScreenConfigto indicate the target configuration:origin,region, andframe-rate. TheDoStartimplementation must then set theConfigproperty 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 thedataparameter represents. You must calculate the duration based on your implementation. If the audio source is raising uncompressed (PCM) audio data, you can infer thedurationdirectly 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 theVideoSourceobject that theAudioSourceobject is going to synchronize with. To synchronize audio with a video source, like for lip syncing, set theAudioSourceobject'sOutputSynchronizableproperty totruein 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.AudioFormatof the audio data. You can use theOutputFormatproperty of your audio source to retrieve this.Wrap the audio buffer in an instance of
FM.LiveSwitch.AudioFrameand provide the audio duration.Set the
AudioFrame's SystemTimestamp.Invoke
RaiseFrameon this newAudioFrameinstance.
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.VideoFormatof the data as well as the width and height of the video. You can use theOutputFormatproperty 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.VideoFrameand 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
Durationproperty of theAudioFrameparameter. - Retrieve the
DataBufferproperty ofAudiobuffer, and then retrieve the raw audio data through theDataproperty of thisDataBufferinstance. - 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
WidthandHeightproperties of the video buffer parameter. - Retrieve the
DataBufferproperty of the same parameter, and then retrieve the raw video data through theDataproperty of theDataBufferinstance. - Pass these values into the
VideoRenderObject(or whatever interface you are using for this sink.) - Finally, Implement
DoDestroyto 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;
}
}