Migration Guide for Android SDK

This migration guide provides some tips for upgrading or downgrading between releases of the Android SDK.

👍

Getting Started with Android SDK

If you haven't already, begin by following the Getting Started tutorials to become familiar with the concepts to create an application that can publish and/or subscribe using the Android SDK.


1.8.x to 2.x

Below you'll find examples for migrating your applications from v1.8.9 to v2.0.0 of the Android SDK.


NOTE: Publisher classes remain unchanged and do not require any migration.


Subscribing Single Source Streams

For applications that only have a single source available for playback, track management has changed.

Core.initialize()

val subscriber = Core.createSubscriber()

launch {
    subscriber.state.map { it.connectionState }.distinctUntilChanged().collect {
        if (it == SubscriberConnectionState.Connected) {
            subscriber.subscribe()
        }
    }
}

launch {
    subscriber.track.collect { trackHolder ->
        when(trackHolder) {
            is TrackHolder.VideoTrackHolder -> {
                // Create the renderer:
                val view = com.millicast.video.TextureViewRenderer(applicationContext)
                // Add the renderer to the layout:
                val layout = findViewById<LinearLayout>(R.id.linear_layout_video)
                layout.addView(view)
                // Set the renderer as a sink on the video track:
                trackHolder.videoTrack.setVideoSink(view)
            }
            is TrackHolder.AudioTrackHolder -> {
                // No video, ignore
            }
        }
}

launchDefaultScope {
    val credentials = Credential(
        streamName = "STREAM_NAME",
        accountId = "ACCOUNT_ID",
        apiUrl = "https://director.millicast.com/api/director/subscribe"
    )

    subscriber.setCredentials(credentials)
    subscriber.connect()
}
Core.initialize()

val subscriber = Core.createSubscriber()

launch {
    subscriber.state.map { it.connectionState }.distinctUntilChanged().collect {
        if (it == SubscriberConnectionState.Connected) {
            subscriber.subscribe()
        }
    }
}

launch {
  subscriber.onRemoteTrack.collect { trackHolder ->
      when (trackHolder) {
          is RemoteAudioTrack -> {
            trackHolder.enableAsync()
          }

          is RemoteVideoTrack -> {
            trackHolder.enableAsync {videoSink = <The TextureViewRenderer View>}
          }
      }
  }
}

launchDefaultScope {
    val credentials = Credential(
        streamName = "STREAM_NAME",
        accountId = "ACCOUNT_ID",
        apiUrl = "https://director.millicast.com/api/director/subscribe"
    )

    subscriber.setCredentials(credentials)
    subscriber.connect()
}

Video view renderer using TextureViewRenderer remains the same in 2.0.0.

Subscribing Multi-View Streams

There were a lot of complicated steps involved in projecting the sources for a multi-stream view. This complex workflow requires a lot of understanding of the WebRTC remote tracks. Using the new 2.0.0 implementation removes the need to handle sourceIds, mids and the projection data directly. This solves a lot of complexities from the old implementation.

//Using the multi-source feature requires projecting tracks into a specified transceiver using its media ID (mid). When you start subscribing, you will receive the activity flow updates. To forward the selected media to the subscriber, use the project method of the subscriber. It requires providing the source ID you are targeting and an array of the tracks you want to project. And for more advanced cases, you may need to unproject and project tracks again. So you have to store a map of <sourceId, RemoteTrackHolder> for later usage. To achieve this you should listen to the StreamSourceActivity flow, then for every new track add only the new tracks to this stored map. Which is a big effort to do and a lot of stuff to worry about, as an example WHEN to re-init this map and etc.

Core.initialize()

val subscriber = Core.createSubscriber()

launch {
    subscriber.state.map { it.connectionState }.distinctUntilChanged().collect {
        if (it == SubscriberConnectionState.Connected) {
            subscriber.subscribe()
        }
    }
}

// Initialize the local items important for this local sample
val streamIdMap = mutableMapOf<String, StreamSourceActivity>()
val queue = Queue()
// com.millicast.utils.Queue provides a scope.launch { } that will wait
// for its head to complete before calling the next item

/**
 * Retrieve the list of new tracks given modifications on the StreamSourceActivity object
 */
fun checkTracksInStreamSourceActivity(streamSourceActivity: StreamSourceActivity): StreamSourceActivity {
  // First, ensure that you find a corresponding cached version
  streamIdMap.putIfAbsent(
    streamSourceActivity.streamId,
    streamSourceActivity.copy(activeTracks = emptyArray())
  )

  val cached = streamIdMap[streamSourceActivity.streamId]!!

  // All the new tracks, for example each track that is in the new array but not the previous
  val newTracks = streamSourceActivity.activeTracks.filter { activeTrack ->
    null == cached.activeTracks.find { it.trackId == activeTrack.trackId }
  }

  // Send a copy that contains only the new tracks
  return streamSourceActivity.copy(activeTracks = newTracks.toTypedArray())
}

/**
 * When a new source becomes active, store information about it, create a local track,
 * and send the projection command
 */
launch {
  subscriber.state.map { it.streamSourceActivities }.collect { streamSourceActivities ->
    queue.post {
      // Check whether the list of activity or tracks
      // is empty, which means that everything local needs to be flushed

      streamSourceActivities.map { activity ->
        // Fetch a copy of the activity object containing only the new tracks
        val newlyFoundRemoteTracks = checkTracksInStreamSourceActivity(activity)

        // Update the cached list of activeTracks with the one obtained at the beginning
        streamIdMap[activity.streamId] = streamIdMap[activity.streamId]!!.copy(
          activeTracks = activity.activeTracks
        )

        newlyFoundRemoteTracks
      }.forEach { activity ->
        // The activity only contains the newly added tracks
        activity.activeTracks.forEach {
          // You can add the remote track locally
          val newlyAddedLocalTrack = subscriber.addRemoteTrackForResult(it.media)

          // Send the command to project the remote content locally
          subscriber.project(
            activity.sourceId, arrayListOf(
              ProjectionData(
                trackId = it.trackId,
                media = it.media.name.lowercase(),
                mid = newlyAddedLocalTrack.mid!!
              )
            )
          )
        }
      }
    }
  }
}

launchDefaultScope {
    val credentials = Credential(
        streamName = "STREAM_NAME",
        accountId = "ACCOUNT_ID",
        apiUrl = "https://director.millicast.com/api/director/subscribe"
    )

    subscriber.setCredentials(credentials)
    subscriber.connect()
}
Core.initialize()

val subscriber = Core.createSubscriber()

launch {
    subscriber.state.map { it.connectionState }.distinctUntilChanged().collect {
        if (it == SubscriberConnectionState.Connected) {
            subscriber.subscribe()
        }
    }
}

launch {
  subscriber.onRemoteTrack.collect { trackHolder ->
      when (trackHolder) {
          is RemoteAudioTrack -> {
            trackHolder.enableAsync()
          }

          is RemoteVideoTrack -> {
            trackHolder.enableAsync {videoSink = <The TextureViewRenderer View>}

            trackHolder.onState.collectInLocalScope { trackState ->
                if (!trackState.isActive) {
                    // Either call trackHolder.disableAsync() and remove the video renderer view or show an UI message till source comes back online
                }
            }
          }
      }
  }
}

launchDefaultScope {
    val credentials = Credential(
        streamName = "STREAM_NAME",
        accountId = "ACCOUNT_ID",
        apiUrl = "https://director.millicast.com/api/director/subscribe"
    )

    subscriber.setCredentials(credentials)
    subscriber.connect()
}

The new 2.0.0 implementation for multi-stream is identical to the single stream view.

Some key differences in track management

1.8.x2.x
Track event emitted is of type TrackHolder which has two types AudioTrackHolder(mid: String, audioTrack: AudioTrack) and VideoTrackHolder(mid: String, videoTrack: VideoTrack)Track event emits RemoteTrack. This new track type can be a video or audio track, using is RemoteVideoTrack you can verify if it's a video track. Use is RemoteAudioTrack to check if the track is an audio track.
Enabling(projecting) tracks is a multistep process
- Subscribe to SubscriberState.streamSourceActivity flow to collect active and inactive sourceId's
- Receive active tracks using activeTracks async stream
- Request tracks by calling the addRemoteTrack()/addRemoteTrackForResult() function
- Project mid's using the project() API by passing in the ProjectionData
Enabling tracks is now easy
- Collect tracks using the onRemoteTrack flow
- Enable track by calling the enableAsync() for an audio track or enableAsync(videoSink) API for a video track
Disable(unproject) tracks using the unproject(mids) functionDisable tracks using the disableAsync()function
layers flow is part of Subscriberlayers flow is now part of RemoteTrack
To select a layer create a new ProjectionData, set the appropriate layerData and then call project()To select a layer call the enableAsync() function and pass the LayerDataSelection to select
activity flow was part of the Subscriber. ActivityStream.Inactive event for a particular sourceId indicates the tracks associated with that sourceId are now inactive and ActivityStream.Active event represents the tracks associated with the sourceId in the event is now activeisActive is now part of the Subscriber.onRemoteTrack.

Changelog Overview

Please refer to the changelog for What's new in 2.0.0 SDK Release