Lumen help

Legacy integration for ExoPlayer on Android

Project setup

                repositories {
    maven {
        url 'https://sdk.streamroot.io/android'
    }
}
            
                implementation 'io.streamroot.dna:dna-core:3.23.0'
            
                android {
// Configure only for each module that uses Java 8
// language features (either in its source code or
// through dependencies).
compileOptions {
      sourceCompatibility JavaVersion.VERSION_1_8
      targetCompatibility JavaVersion.VERSION_1_8
  }
}

            

Integrate Lumen SDK

                <meta-data
        android:name="io.streamroot.dna.StreamrootKey"
        android:value="streamrootKey"/>

            
                public final class SRApplication extends MultiDexApplication {
    @Override
    public void onCreate() {
        super.onCreate();
        DnaClient.initializeApp(this);
    }
}

            
                android:name=".SRApplication"
            
                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))

        streamStatsManager = StreamStatsManager.newStatsManager(dnaClient, streamrootDnaStatsView)
    } catch (e: Exception) {
        Toast.makeText(applicationContext, e.message, Toast.LENGTH_LONG).show()
    }

    return dnaClient
}

            
                DefaultLoadControl.Builder()
        .setBufferDurationsMs(10_000, // overrides default DEFAULT_MIN_BUFFER_MS
                DEFAULT_MAX_BUFFER_MS,
                DEFAULT_BUFFER_FOR_PLAYBACK_MS,
                DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
        .createDefaultLoadControl();
            
                import android.os.Looper
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.LoadControl
import com.google.android.exoplayer2.Timeline
import io.streamroot.dna.core.PlayerInteractor
import io.streamroot.dna.core.TimeRange
import java.util.concurrent.TimeUnit

private interface BufferTargetBridge {
    fun bufferTarget() : Double = 0.0
    fun setBufferTarget(bufferTarget: Double) {}
}

private class BufferTargetBridgeDefault : BufferTargetBridge

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())
        if (maxBufferUs > minBufferUs) runCatching {
            maxBufferField.setLong(
                    loadControl,
                    maxBufferUs
            )
        }
    }
}

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)
}

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
    )
}

private object BufferTargetBridgeFactory {
    fun createInteractor(loadControl: LoadControl, audioOnly: Boolean) : BufferTargetBridge {
        return runCatching { LoadControlBufferTargetBridgeV1(loadControl) }.getOrNull()
            ?: runCatching { LoadControlBufferTargetBridgeV2(loadControl, audioOnly) }.getOrNull()
            ?: BufferTargetBridgeDefault()
    }
}

class ExoPlayerInteractor(
        private val player: ExoPlayer,
        loadControl: LoadControl,
        audioOnly: Boolean = false
) : PlayerInteractor {

    private val bridge = BufferTargetBridgeFactory.createInteractor(loadControl, audioOnly)

    override fun looper(): Looper = player.applicationLooper

    override fun loadedTimeRanges(): List<TimeRange> {
        val shift = getCurrentWindowShift()
        val rangeDurationMs = player.bufferedPosition - player.currentPosition
        return if (rangeDurationMs > 0) {
            arrayListOf(TimeRange(shift + player.currentPosition, rangeDurationMs))
        } else {
            emptyList()
        }
    }

    override fun playbackTime(): Long {
        return getCurrentWindowShift() + player.currentPosition
    }

    private fun getCurrentWindowShift(): Long {
        val current = player.currentTimeline
        val timelineWindow = Timeline.Window()
        var shift: Long = 0

        if (player.currentWindowIndex < current?.windowCount!!) {
            player.currentTimeline?.getWindow(player.currentWindowIndex, timelineWindow)
            shift = timelineWindow.positionInFirstPeriodMs
        }

        return shift
    }

    override fun bufferTarget() = bridge.bufferTarget()
    override fun setBufferTarget(bufferTarget: Double) = bridge.setBufferTarget(bufferTarget)
}

            
                #ExoPlayer
-keep class com.google.android.exoplayer2.** { *; }
-keep interface com.google.android.exoplayer2.** { *; }
            
                import android.os.Handler
import com.google.android.exoplayer2.upstream.BandwidthMeter
import com.google.android.exoplayer2.upstream.TransferListener
import io.streamroot.dna.core.BandwidthListener
import java.util.concurrent.atomic.AtomicLong

class ExoPlayerBandwidthMeter : BandwidthMeter, BandwidthListener {

    override fun getTransferListener(): TransferListener? = null

    override fun addEventListener(eventHandler: Handler?, eventListener: BandwidthMeter.EventListener?) { }

    override fun removeEventListener(eventListener: BandwidthMeter.EventListener?) { }

    private val estimatedBandwidth = AtomicLong(0L)

    override fun getBitrateEstimate(): Long {
        return estimatedBandwidth.get()
    }

    override fun onBandwidthChange(estimatedBandwidth: Long) {
        this.estimatedBandwidth.set(estimatedBandwidth)
    }
}

            
                DnaClient dnaClient = DnaClient.newBuilder()
     .context(<Context>)
     .playerInteractor(ExoPlayerInteractor(newPlayer, loadControl()))
     ...

     .bandwidthListener(<BandwidthListener>)

     ...
     .start(<String>);

            
                import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.PlaybackParameters
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.Player.STATE_BUFFERING
import com.google.android.exoplayer2.Player.STATE_ENDED
import com.google.android.exoplayer2.Player.STATE_IDLE
import com.google.android.exoplayer2.Player.STATE_READY
import com.google.android.exoplayer2.Timeline
import com.google.android.exoplayer2.source.TrackGroupArray
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
import io.streamroot.dna.core.PlaybackState
import io.streamroot.dna.core.QosModule

class ExoPlayerQosModule(
    exoPlayer: ExoPlayer
) : QosModule(), Player.EventListener {

    init {
        exoPlayer.addListener(this)
    }

    override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {}

    override fun onSeekProcessed() {
        playbackStateChange(PlaybackState.SEEKING)
    }

    override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {
        trackSwitchOccurred()
    }

    override fun onPlayerError(error: ExoPlaybackException?) {
        playbackErrorOccurred()
    }

    override fun onLoadingChanged(isLoading: Boolean) {}

    override fun onPositionDiscontinuity(reason: Int) {}

    override fun onRepeatModeChanged(repeatMode: Int) {}

    override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {}

    override fun onTimelineChanged(timeline: Timeline?, manifest: Any?, reason: Int) {}

    override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
        when (playbackState) {
            STATE_IDLE -> {
                playbackStateChange(PlaybackState.IDLE)
            }
            STATE_BUFFERING -> {
                playbackStateChange(PlaybackState.BUFFERING)
            }
            STATE_READY -> {
                playbackStateChange(if (playWhenReady) PlaybackState.PLAYING else PlaybackState.PAUSING)
            }
            STATE_ENDED -> {
                playbackStateChange(PlaybackState.ENDED)
            }
        }
    }
}

            
                DnaClient dnaClient = DnaClient.newBuilder()
     .context(<Context>)
     .playerInteractor(ExoPlayerInteractor(newPlayer, loadControl()))
     ...

     .qosModule(ExoPlayerQosModule(<SimpleExoPlayer>))

     ...
     .start(<String>);

            
                dnaClient.close();
            
                <?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    <application
        ...
        android:usesCleartextTraffic="true">
        ...
    </application>
</manifest>
            
                <?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>
            
                <?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    <application
        ...
        android:networkSecurityConfig="@xml/network_security_config">
        ...
    </application>
</manifest>
            
                @SuppressLint("SwitchIntDef")
    private fun buildMediaSource(uri: Uri): MediaSource {
        val defaultDataSourceFactory =
            DefaultHttpDataSourceFactory(
                    Util.getUserAgent(applicationContext, "StreamrootQA"),
                    DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
                    DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
                    true
            )

        return when (Util.inferContentType(uri)) {
            C.TYPE_HLS -> HlsMediaSource.Factory(defaultDataSourceFactory)
                .createMediaSource(uri)
            C.TYPE_DASH -> DashMediaSource.Factory(
                DefaultDashChunkSource.Factory(
                    defaultDataSourceFactory
                ), defaultDataSourceFactory
            )
                .createMediaSource(uri)
            else -> {
                throw IllegalStateException("Unsupported type for url: $uri")
            }
        }
    }