In version 3.8 of our Android SDK, we have introduced some changes to our API. This article details the main changes and how to handle them.
1. Buffer Levels
Before 3.7
Buffer levels where randomly set at player instantiation between a minimum and maximum target.
We had 2 ways to do so depending on the version of ExoPlayer.
Before ExoPlayer 2.8.0
It was required to change the instantiation of the default Exoplayer class DefaultLoadControl
with this one:
DefaultLoadControl(
DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
DnaClient.generateBufferTarget(
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
latencyInSec,
TimeUnit.SECONDS),
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES,
DefaultLoadControl.DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS)
From ExoPlayer 2.8.0
It was required to change the instantiation of the default Exoplayer builder DefaultLoadControl
with this one:
DefaultLoadControl.Builder()
.setAllocator(DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE))
.setBufferDurationsMs(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
DnaClient.generateBufferTarget(
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
latencyInSec,
TimeUnit.SECONDS),
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS)
.createDefaultLoadControl()
From 3.8
We now dynamically set the buffer targets to optimize DNA efficiency. It works for all ExoPlayer version.
The only changes made is the addition of this setter and getter in the Player Interactor
override fun bufferTarget(): Double {
return runCatching { maxBufferField.getLong(loadControl).let { TimeUnit.MICROSECONDS.toSeconds(it) }.toDouble() }.getOrNull()
?: 0.0
}
override fun setBufferTarget(target: Double) {
val maxBufferUs = TimeUnit.SECONDS.toMicros(target.toLong())
if (maxBufferUs > minBufferUs) runCatching { maxBufferField.setLong(loadControl, maxBufferUs) }
}
On Live streaming, to improve offload efficiency, we recommend to change the default value of the minimal buffer target to 10s. In order to do so, you need to change the instantiation of the default Exoplayer class DefaultLoadControl
with this one:
DefaultLoadControl.Builder()
.setBufferDurationsMs(10_000,
DEFAULT_MAX_BUFFER_MS,
DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
.createDefaultLoadControl();
2. Player Interactor
Before 3.7
The PlayerInteractor is how the SDK gets player specifics. The SDK needed the following information to work optimally:
- Looper
looper must return the looper used to create the player. It is mandatory from ExoPlayer 2.9.0 and above.
The recommended way is to use theapplicationLooper
function (ExoPlayer doc) - Loaded TimeRanges
loadedTimeRanges must return a list of TimeRange (in milliseconds).
Each TimeRange represents a continuous interval of content present in the buffer. It is mandatory to return at least the TimeRange going from the playback time to the end of the buffered interval containing the playback time. - Current playback time
playbackTime must return the current time of the player in milliseconds.
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.Timeline
import io.streamroot.dna.core.PlayerInteractor
import io.streamroot.dna.core.TimeRange
class ExoPlayerInteractor(
private val player: SimpleExoPlayer
) : PlayerInteractor {
//From ExoPlayer 2.9.0 and above
override fun looper(): Looper? = player.applicationLooper
override fun loadedTimeRanges(): MutableList<TimeRange> {
val shift = getCurrentWindowShift()
val rangeDurationMs = player.bufferedPosition - player.currentPosition
val timeRanges: MutableList<TimeRange> = ArrayList()
if (rangeDurationMs > 0) {
timeRanges.add(
TimeRange(
shift + player.currentPosition,
rangeDurationMs
)
)
}
return timeRanges
}
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
}
}
From 3.8
The SDK now needs the following information to work optimally:
The PlayerInteractor is how the SDK gets player specifics. The SDK will need the following information to work optimally:
- Looper
looper must return the looper used to create the player. It is mandatory from ExoPlayer 2.9.0 and above.
The recommended way is to use theapplicationLooper
function (ExoPlayer doc) - Loaded TimeRanges
loadedTimeRanges must return a list of TimeRange (in milliseconds).
Each TimeRange represents a continuous interval of content present in the buffer. It is mandatory to return at least the TimeRange going from the playback time to the end of the buffered interval containing the playback time. - Current playback time
playbackTime must return the current time of the player in milliseconds. - Buffer target
bufferTarget must return the target the player will try to buffer.
setBufferTarget allows us to dynamically change the buffer target of the player to optimize DNA performances.
import android.os.Looper
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Timeline
import io.streamroot.dna.core.PlayerInteractor
import io.streamroot.dna.core.TimeRange
import com.google.android.exoplayer2.LoadControl
import java.lang.reflect.Field
import java.util.concurrent.TimeUnit
class ExoPlayerInteractor(
private val player: ExoPlayer,
private val loadControl: LoadControl,
maxBufferFieldName: String = "maxBufferUs",
minBufferFieldName: String = "minBufferUs"
) : PlayerInteractor {
private val minBufferUs: Long
private val maxBufferField: Field
init {
minBufferUs = runCatching {
val minBufferField = loadControl::class.java.getDeclaredField(minBufferFieldName)
minBufferField.isAccessible = true
minBufferField.getLong(loadControl)
}.getOrNull() ?: throw IllegalArgumentException("Impossible to retrieve minBuffer field `$minBufferFieldName` value from LoadControl of type `${loadControl::class.java.simpleName}`")
maxBufferField = runCatching { loadControl::class.java.getDeclaredField(maxBufferFieldName) }.getOrNull()
?: throw IllegalArgumentException("Impossible to retrieve maxBuffer field `$maxBufferFieldName` from LoadControl of type `${loadControl::class.java.simpleName}`")
maxBufferField.isAccessible = true
}
override fun looper(): Looper = player.applicationLooper
override fun bufferTarget(): Double {
return runCatching { maxBufferField.getLong(loadControl).let { TimeUnit.MICROSECONDS.toSeconds(it) }.toDouble() }.getOrNull()
?: 0.0
}
override fun setBufferTarget(target: Double) {
val maxBufferUs = TimeUnit.SECONDS.toMicros(target.toLong())
if (maxBufferUs > minBufferUs) runCatching { maxBufferField.setLong(loadControl, maxBufferUs) }
}
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
}
}
As we use this API at a low frequency, we've decided to uses reflection for this implementation.
It is therefore required to blacklist all ExoPlayer classes when using either Proguard like so
#ExoPlayer
-keep class com.google.android.exoplayer2.** { *; }
-keep interface com.google.android.exoplayer2.** { *; }
3. DNA Client instance creation
Before 3.7
DnaClient dnaClient = DnaClient.newBuilder()
.context(<Context>)
.playerInteractor(<PlayerInteractor>)
.latency(<Integer>)
.start(<Uri>);
From 3.8
DnaClient dnaClient = DnaClient.newBuilder()
.context(<Context>)
.playerInteractor(ExoPlayerInteractor(newPlayer, loadControl()))
.latency(<Integer>)
.start(<Uri>);