Getting Started with Subscribing
Follow these steps to add the subscribing capability to your application.
1. Add SDK to Gradle
You can get the SDK library from MavenCentral. If you haven't already, add the following to your gradle dependencies.
implementation("com.millicast:millicast-sdk-android:2.0.0")
2. Initialize the SDK
Call the initialize method to initialize the SDK. This needs to be done only once at the start of the App.
import android.app.Application
import com.millicast.Core
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
Core.initialize()
}
}
3. Create a subscriber object
Use the createSubscriber method to create a subscriber object.
launchDefaultScope {
// Creating the subscriber
val subscriber = Core.createSubscriber()
}
4. Setup your credentials
Get your stream name and stream ID from the dashboard and set them up in the SDK using the setCredentials method.
launchDefaultScope {
val credentials = Credential(
streamName = "STREAM_NAME",
accountId = "ACCOUNT_ID",
apiUrl = "https://director.millicast.com/api/director/subscribe"
)
subscriber.setCredentials(credentials)
}
5. Subscribe to a stream
Subscribing a stream includes two steps.
5.1 Connect to the Millicast service
Define your connection options and connect to the Millicast platform.
launchDefaultScope {
// autoReconnect - Set to `true` if you would like the SDK to reconnect to stream on a connection error; for example - when
// the network is lost and later restored.
val connectionOptions = ConnectionOptions(autoReconnect: true)
subscriber.connect(connectionOptions)
}
5.2 Subscribe with the preferences
Once the connection is successful, call the subscribe method with your preferred subscribe options. To monitor the connection state, please refer to the section 7 - Listen to Websocket and Peer connection state changes.
launchDefaultScope {
subscriber.state.map { it.connectionState }.distinctUntilChanged().collect {
if (it == SubscriberConnectionState.Connected) {
val option = Option(
// The main source that will be received by the default media stream
pinnedSourceId = "mainSource",
// Enables audio multiplexing and denotes the number of audio tracks to receive as Voice Activity Detection (VAD)
// multiplexed audio
multiplexedAudioTrack = 3U,
// Audio streams that should not be included in the multiplex, for example your own audio stream
excludedSourceId = arrayOf("excluded")
)
subscriber.subscribe(option)
}
}
}
Refer to Option for more subscriber options.
6. Manage broadcast events
When broadcast events occur, the SDK publishes the update to one of the flows maintained by the client object. The following Subscriber event listeners are available:
6.1 Receive Audio and Video tracks
RemoteTrack is a fundamental entity that enables us to view a stream. It can either be a video track (RemoteVideoTrack) or an audio track (RemoteAudioTrack). You can just call enableAsync() or disableAsync() to enable or disable the track.
You should store the tracks and renderers for later use.
Important: Audio and Video tracks of the same source will have the same sourceId
launch {
subscriber.onRemoteTrack.collect { trackHolder ->
when (trackHolder) {
is RemoteAudioTrack -> {
// Store audio tracks for later usage
audioTracks.add(trackHolder)
}
is RemoteVideoTrack -> {
// Store video tracks for later usage
videoTracks.add(trackHolder)
}
}
}
}
6.2 Listen to Active/Inactive state of tracks
For each RemoteTrack, listen to its active and inactive state by moniting its onState
stateFlow. This will receive a RemoteVideoTrackState object with an isActive
field, which signals if that track is available for playback or not.
Note: A video track receives video frames only when its enabled.
launch{
remoteTrack.onState.collect { trackState ->
Log.d(TAG, "onVideoTrack state ${trackState.mid}, ${trackState.isActive}")
if (!trackState.isActive) {
// Optional.
// The SDK automatically restores the state of the track when it transitions to `active` from an `inactive` state.
// You can optionally disable the video track when it becomes inactive. This step is optional. This gives you control on when to enable the track when it comes back active.
videoTrack.disableAsync()
} else {
// Optional.
// If you choose to disable a track when it became inactive, you have to enable the video track back after it is active again.
// At any point in time when you wish to start receive video from the track call the following.
videoTrack.enableAsync(videoSink = videoSink)
}
}
}
Same can be done for AudioTrack.
6.3 Receive layer information
If your video track has multiple layers(spatial or temporal), use the layers stateFlow to receive the list of active layers.
To receive layers, collect Layers data emitted from RemoteVideoTrackState.
To select a particular layer, re-enable the track by passing the layer that you would like to select.
videoTrack.onState.collect { trackState ->
trackState.layers?.let { layers ->
video.enableAsync(videoSink = videoSink, layer = layers.activeLayers[0])
}
7. Listen to Websocket and Peer connection state changes
When broadcast events occur, the SDK publishes the update to one of the flows maintained by the client object. The following Subscriber event listeners are available:
subscriber.state.map { it.connectionState }.distinctUntilChanged()
.collect { state ->
when (state) {
SubscriberConnectionState.Connected -> {}
SubscriberConnectionState.Connecting -> {}
SubscriberConnectionState.Disconnected -> {}
is SubscriberConnectionState.DisconnectedError -> {}
SubscriberConnectionState.Disconnecting -> {}
is SubscriberConnectionState.Error -> {}
SubscriberConnectionState.Stopped -> {}
SubscriberConnectionState.Subscribed -> {}
}
}
subscriber.state.map { it.websocketConnectionState }.distinctUntilChanged().collect {}
subscriber.state.map { it.peerConnectionState }.distinctUntilChanged().collect {}
8. Render video on the UI
The SDK provides a custom View (TextureViewRenderer) for rendering a video track. Use TextureViewRenderer as in the example below:
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
modifier = Modifier
.aspectRatio(16F / 9)
.align(Alignment.Center),
factory = {
TextureViewRenderer(context).apply {
init(Media.eglBaseContext, null)
}
},
update = { view ->
view.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
videoTrack.enableAsync(videoSink = view)
}
)
}
9. Collecting RTC statistics
You can periodically collect the WebRTC peer connection statistics if you enable them through the enableStats() method of the subscriber. After enabling the statistics, you will get a report every second through the rtcStatsReport stateFlow.
The identifiers and way to browse the stats are following the RTC specification. The report contains the RtsReport object, which is a collection of several RtpStream objects. They all have a specific type, whether it is inbound, outbound, codec, or media. Inbound is the statistics of incoming transport for the viewer and outbound is a type of outgoing statistics for the publisher.
subscriber.rtcStatsReport.collect { report ->
// Parse the stats report for logging or display on to the user interface
}
10. Error handling
To listen to the errors emitted by the subscriber, listen to the connectionState of the subscriber.state as described in section 7. In addition to that there are additional two state events that can be monitored for errors. Subscriber also provides notification for any signaling error in subscriber.signalingError.
subscriber.state.map { it.peerConnectionState }.distinctUntilChanged().collect {}
subscriber.state.map { it.websocketConnectionState }.distinctUntilChanged().collect {}
subscriber.signalingError.collect {}
11. Unsubscribe and disconnect the session
And finally, when you have finished viewing the stream, stop the subscription by calling unsubscribe() which tells the streaming server that the subscriber is no longer interested in receiving audio and video content. Then disconnect the websocket connection with the server by calling the disconnect() method.
subscriber.unsubscribe()
subscriber.disconnect()
And in some use cases such as subscribing to a new stream, within the same fragment or composable screen, the best practice would be to call subscriber.release().This will internally release all listeners and resources so there is no need to call unsubscribe or disconnect. Then for a new stream, create a new subscriber instance and follow the same connection and subscription process described above. Also consider releasing the VideoSink and TextureViewRenderer to inform the SDK to stop rendering frames on that surface.
Note that in this case if you want to release everything and create a new subscriber, consider also releasing the VideoSink listener and the TextureViewRenderer. This way you are telling the SDK to stop rendering video frames on the surface.
subscriber.release() // Always make sure that in this case the subscriber instance is released 100%, so it is recommended to call it directly before creating //a new subscriber
// And in between it is recommended to release your rendererView and remove the Video Sink listener from the videoTrack
videoTrack.removeVideoSink(textureRendererView)
textureRendererView.release()
// Now everything is cleaned well so that you can create a new subscriber.
subscriber = Core.createSubscriber()
Updated 3 months ago