Overview
This article explains all the changes that need to be fulfilled to migrate to the Android SDK 22.X and benefit from the major performance and usability improvements available.
The Android SDK 22.X ships the branding transition to Lumen and the rework of our internal modules architecture.
We have made the fewest changes to the integration and kept it as simple as possible.
Most of the changes concern renaming from "streamroot" to "lumen" and from "DNA" to "MESH".
The complete integration steps to follow for a brand new integration can be found at ExoPlayer from SDK 22.01.
In summary
Footprint
Compared to previous versions:
- 2x less average user CPU time
- 4x less average RAM usage, runtime is ~17MB (plus configurable P2P cache size, by default 60MB for live and 80MB for VOD)
- Lower device hardware requirement compatibility (e.g. 4cores CPU, 1GB RAM)
SDK size
- Total bundle 14-17MB (final size depends on the device CPU architecture)
Features
- Increased CDN Offload: expect +5-20% of P2P offload
- Reduced footprint on low-end devices
- Improved reaction to network speed changes
- Additional client side debugging tools, via Stats View external library
Versioning scheme
The SDK now uses a date-based versioning scheme.
It is based on Year, Month, Patch as follows: YY.MM.patch
Required changes
Dependency
The full dependency of the SDK changed from io.streamroot.dna:dna-core
to io.streamroot.lumen.delivery.client:mesh-sdk
.
It is now declared as follows:
Before 22.01
implementation 'io.streamroot.dna:dna-core:3.23.0'
From 22.01
implementation 'io.streamroot.lumen.delivery.client:mesh-sdk:22.01.0'
Unique customer key setting
The unique identifier that you can find in the Account section of the Lumen Dashboard has been renamed fromstreamrootKey
to deliveryClientKey
.
It must be set as a meta-data in the application manifest as follows:
Before 22.01
<meta-data
android:name="io.streamroot.dna.StreamrootKey"
android:value="streamrootKey"/>
From 22.01
<meta-data
android:name="io.streamroot.lumen.delivery.client.DeliveryClientKey"
android:value="DCKey" />
SDK Initialization
The client has been renamed from DnaClient
to LumenDeliveryClient
.
It must be set as a meta-data in the application manifest as follows:
Before 22.01
class SRApplication extends MultiDexApplication {
@Override
public void onCreate() {
super.onCreate();
DnaClient.initializeApp(this, "srKey");
}
}
From 22.01
class SRApplication: MultiDexApplication() {
override fun onCreate() {
super.onCreate()
LumenDeliveryClient.initializeApp(this, "DCKey")
}
}
Lumen Delivery Client instance creation, start and stop
The way to create the Lumen Delivery Client instances has changed in many ways from initStreamroot
to initDeliveryClient
and the start is now done in a separate method.
It is also not needed to pass the identifier at this stage anymore.
Before 22.01
Creation and start of the Delivery Client instance are done at the same time as follows:
private fun initStreamroot(newPlayer: ExoPlayer, loadControl: LoadControl): DnaClient? {
var dnaClient: DnaClient? = null
try {
dnaClient = DnaClient.newBuilder()
.context(applicationContext)
.playerInteractor(ExoPlayerInteractor(newPlayer, loadControl, false))
.streamrootKey(<String>)
.latency(latency)
.start(Uri.parse(mStreamUrl))
return dnaClient
}
From 22.01
Creation of the Delivery Client instance is done as follows:
private fun initDeliveryClient(newPlayer: SimpleExoPlayer, loadControl: LoadControl) =
LumenDeliveryClient.meshBuilder(applicationContext)
.playerInteractor(ExoPlayerInteractor(newPlayer, loadControl, false))
.options {
contentId(<string>)
meshProperty(<string>)
latency(latency)
logLevel(<string>)
}
.build(mStreamUrl)
And calling the start()
method on the Delivery Client instance will start the SDK.
deliveryClient.start()
Get new local URL
The method has been renamed from dnaClient.manifestUrl()
to deliveryClient.localUrl()
.
You must retrieve the new local URL as follows:
Before 22.01
val finalUrl = dnaClient.manifestUrl()
From 22.01
val finalUrl = deliveryClient.localUrl()
Integrate the Player interactor and QoS module
A lots of changes have been made across the player interactor to always ensure best QOS and efficiency.
The PlayerInteractor
, the BandwidthMeter
and the QosModule
can now be part of the same class.
Before 22.01
See the PlayerInteractor.
See the BandwidthMeter.
See the QosModule.
From 22.01
class PlayerInteractor(
private val player: ExoPlayer,
loadControl: LoadControl,
private val bandwidthMeter: ExoPlayerBandwidthMeter
) : LumenPlayerInteractorBase(),
Player.Listener {
init {
player.addListener(this)
}
override fun onSeekProcessed() {
super.playerStateChange(LumenVideoPlaybackState.SEEKING)
}
override fun onTracksChanged(trackGroups: TrackGroupArray, trackSelections: TrackSelectionArray) {
super.playerTrackSwitch()
}
override fun onPlayerError(error: PlaybackException) {
super.playerError()
}
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
when (playbackState) {
Player.STATE_IDLE -> {
super.playerStateChange(LumenVideoPlaybackState.IDLE)
}
Player.STATE_BUFFERING -> {
super.playerStateChange(LumenVideoPlaybackState.REBUFFERING)
}
Player.STATE_READY -> {
super.playerStateChange(if (playWhenReady) LumenVideoPlaybackState.PLAYING else LumenVideoPlaybackState.PAUSED)
}
Player.STATE_ENDED -> {
super.playerStateChange(LumenVideoPlaybackState.ENDED)
}
}
}
private val handler = Handler(player.applicationLooper)
private fun <T> runSyncOnEPHandler(block: () -> T): T? {
var t: T? = null
val lock = Object()
synchronized(lock) {
handler.post {
synchronized(lock) {
t = block()
lock.notify()
}
}
lock.wait()
}
return t
}
// Conversion from milliseconds to seconds does not use TimeUnit because we want
// to keep a floating precision
private val bridge = BufferTargetBridgeFactory.createInteractor(loadControl)
override fun bufferHealth() = runSyncOnEPHandler { (player.bufferedPosition - player.currentPosition) / 1000.0 }!!
override fun bufferTarget() = runSyncOnEPHandler { bridge.bufferTarget() }!!
override fun setBufferTarget(target: Double) = runSyncOnEPHandler { bridge.setBufferTarget(target) }!!
override fun setEstimatedBandwidth(bps: Long?) {
bps?.let {
bandwidthMeter.setEstimatedBandwidth(bps)
}
}
}
class ExoPlayerBandwidthMeter private constructor(context: Context) : BandwidthMeter {
companion object {
fun new(context: Context) = ExoPlayerBandwidthMeter(context)
}
private val estimatedBandwidth = AtomicLong(DefaultBandwidthMeter.Builder(context).build().bitrateEstimate)
fun setEstimatedBandwidth(bps: Long) {
estimatedBandwidth.set(bps)
}
override fun getBitrateEstimate() = estimatedBandwidth.get()
override fun getTransferListener(): TransferListener? = null
override fun addEventListener(eventHandler: Handler, eventListener: BandwidthMeter.EventListener) { }
override fun removeEventListener(eventListener: BandwidthMeter.EventListener) { }
}
internal interface BufferTargetBridge {
fun bufferTarget(): Double = 0.0
fun setBufferTarget(bufferTarget: Double) {}
}
private abstract class LoadControlBufferTargetBridge(protected val loadControl: LoadControl) :
BufferTargetBridge {
protected fun LoadControl.getAccessibleFieldElseThrow(fieldName: String) = runCatching {
val minBufferField = this::class.java.getDeclaredField(fieldName)
minBufferField.isAccessible = true
minBufferField
}.getOrNull() ?: throw IllegalArgumentException("Impossible to retrieve field `$fieldName` value from LoadControl of type `${this::class.java.simpleName}`")
protected fun LoadControl.getLongFromFieldElseThrow(fieldName: String) = runCatching {
getAccessibleFieldElseThrow(fieldName).getLong(this)
}.getOrNull() ?: throw IllegalArgumentException("Impossible to retrieve long `$fieldName` value from LoadControl of type `${this::class.java.simpleName}`")
companion object {
private const val MAX_BUFFER_FIELD_NAME = "maxBufferUs"
}
protected val maxBufferField = loadControl.getAccessibleFieldElseThrow(MAX_BUFFER_FIELD_NAME)
protected abstract val minBufferUs: Long
override fun bufferTarget(): Double {
return runCatching {
maxBufferField.getLong(loadControl).let { TimeUnit.MICROSECONDS.toSeconds(it) }.toDouble()
}.getOrNull() ?: super.bufferTarget()
}
override fun setBufferTarget(bufferTarget: Double) {
val maxBufferUs = TimeUnit.SECONDS.toMicros(bufferTarget.toLong())
runCatching {
maxBufferField.setLong(
loadControl,
maxOf(minBufferUs, maxBufferUs)
)
}
}
}
// 2.10- & 2.12+
private class LoadControlBufferTargetBridgeV1(loadControl: LoadControl) :
LoadControlBufferTargetBridge(loadControl) {
companion object {
private const val MIN_BUFFER_FIELD_NAME = "minBufferUs"
}
override val minBufferUs = loadControl.getLongFromFieldElseThrow(MIN_BUFFER_FIELD_NAME)
}
// 2.11
private class LoadControlBufferTargetBridgeV2(loadControl: LoadControl, audioOnly: Boolean) :
LoadControlBufferTargetBridge(loadControl) {
companion object {
private const val MIN_BUFFER_AUDIO_FIELD_NAME = "minBufferAudioUs"
private const val MIN_BUFFER_VIDEO_FIELD_NAME = "minBufferVideoUs"
}
override val minBufferUs = loadControl.getLongFromFieldElseThrow(
if (audioOnly) MIN_BUFFER_AUDIO_FIELD_NAME else MIN_BUFFER_VIDEO_FIELD_NAME
)
}
internal object BufferTargetBridgeFactory {
fun createInteractor(loadControl: LoadControl, audioOnly: Boolean = false): BufferTargetBridge {
return runCatching { LoadControlBufferTargetBridgeV1(loadControl) }.getOrNull()?.also { Log.v("Misc", "Using interactor V1") }
?: runCatching { LoadControlBufferTargetBridgeV2(loadControl, audioOnly) }.getOrNull()?.also { Log.v("Misc", "Using interactor V2") }
?: throw java.lang.Exception("Unsupported ExoPlayer version")
}
}
Debug tools (Stats View)
The Stats View has been completely redesigned and is part of a new library. More information at Debug tools for Android from SDK 22.01.
Before 22.01
Add the following dependency to the build.gradle
implementation 'io.streamroot.dna:dna-utils:3.23.0'
Update your layout to include the following snippet
<io.streamroot.dna.utils.stats.StatsView
android:id="@+id/streamroot_stats"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Finally, initialize a StreamStatsManager and give it the instance of the SDK you previously instantiated
streamrootDnaStatsView = findViewById(R.id.streamrootDnaStatsView);
streamStatsManager = StreamStatsManager.newStatsManager(<StreamrootSDK>, streamrootDnaStatsView);
From 22.01
Add the following dependency to the build.gradle
implementation 'io.streamroot.lumen.delivery.client:mesh-sdk-utils:22.01.0'
Update your layout to include the following snippet
<io.streamroot.lumen.delivery.client.utils.LumenStatsView
android:id="@+id/statsview_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Retrieve your view by your preferred method and link it with a LumenDeliveryClient
instance
// Helper function to initializing the stats view
private fun initStatsView(dcStatsView: View) {
dcStatsView.apply {
// Instanciate a LumenStatsVie
val statsView = LumenStatsView(context)
lumenDeliveryClient?.addStateStatsListener(statsView)
// Display the stat view. Otherwise it is hidden by default
statsView.showStats()
addView(statsView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
}
dcStatsView = ...
initStatsView(dcStatsView)