Skip to content

Commit

Permalink
feat: Add Session Data for internal experiments (#13)
Browse files Browse the repository at this point in the history
* Add SessionDataBinding

* Hook up session data

* Get rid of convenience function for custom players

* Cleanup

* improve dependency footprint
  • Loading branch information
daytime-em authored Jun 28, 2023
1 parent ab6fa75 commit 94496c3
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 3 deletions.
4 changes: 3 additions & 1 deletion library-exo/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,12 @@ muxDistribution {

dependencies {
api "com.mux.stats.sdk.muxstats:data-media3-custom:$project.version"
api 'androidx.media3:media3-exoplayer:1.0.2' // TODO: Just base and hls
// implementation is used so it doesn't pollute customers namespace
implementation "com.mux:utils-kt:$coreVersion"

api "androidx.media3:media3-exoplayer:$media3Version" // TODO: Not all of exo is required
compileOnly "androidx.media3:media3-exoplayer-hls:$media3Version"

implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,28 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.AnalyticsListener
import androidx.media3.exoplayer.source.MediaLoadData
import com.mux.android.util.weak
import com.mux.stats.sdk.muxstats.internal.createExoSessionDataBinding

class ExoPlayerBinding : MuxPlayerAdapter.PlayerBinding<ExoPlayer> {

private val sessionDataBinding = createExoSessionDataBinding()

private var listener: MuxAnalyticsListener? = null

override fun bindPlayer(player: ExoPlayer, collector: MuxStateCollector) {
listener = MuxAnalyticsListener(player, collector).also { player.addAnalyticsListener(it) }

// Also delegate to sub-bindings
sessionDataBinding.bindPlayer(player, collector)
}

override fun unbindPlayer(player: ExoPlayer, collector: MuxStateCollector) {
listener?.let { player.removeAnalyticsListener(it) }
collector.playerWatcher?.stop("player unbound")
listener = null

// Also delegate to sub-bindings
sessionDataBinding.unbindPlayer(player, collector)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ fun Player.monitorWithMuxData(
customerData = customerData,
player = this,
playerView = playerView,
customOptions = customOptions
customOptions = customOptions,
playerBinding = BaseMedia3Binding()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.mux.stats.sdk.muxstats.internal

import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.AnalyticsListener
import androidx.media3.exoplayer.hls.HlsManifest
import com.mux.android.util.weak
import com.mux.stats.sdk.core.model.SessionTag
import com.mux.stats.sdk.core.util.MuxLogger
import com.mux.stats.sdk.muxstats.MuxPlayerAdapter
import com.mux.stats.sdk.muxstats.MuxStateCollector
import java.util.regex.Matcher
import java.util.regex.Pattern

private class SessionDataPlayerBinding : MuxPlayerAdapter.PlayerBinding<ExoPlayer> {

private var listener: AnalyticsListener? by weak(null)

override fun bindPlayer(player: ExoPlayer, collector: MuxStateCollector) {
if (isHlsExtensionAvailable()) {
listener = SessionDataListener(player, collector).also { player.addAnalyticsListener(it) }
}
}

override fun unbindPlayer(player: ExoPlayer, collector: MuxStateCollector) {
listener?.let { player.removeAnalyticsListener(it) }
}

/**
* Listens for timeline changes and updates HLS session data if we're on an HLS stream.
* This class should only be instantiated if ExoPlayer's HLS extension is available at runtime
* @see [.isHlsExtensionAvailable]
*/
@OptIn(UnstableApi::class)
private class SessionDataListener(player: ExoPlayer, val collector: MuxStateCollector) :
AnalyticsListener {

private val player by weak(player)

companion object {
val RX_SESSION_TAG_DATA_ID: Pattern by lazy { Pattern.compile("DATA-ID=\"(.*)\",") }
val RX_SESSION_TAG_VALUES: Pattern by lazy { Pattern.compile("VALUE=\"(.*)\"") }

/** HLS session data tags with this Data ID will be sent to Mux Data */
const val HLS_SESSION_LITIX_PREFIX = "io.litix.data."
const val LOG_TAG = "SessionDataListener"
}

override fun onTimelineChanged(eventTime: AnalyticsListener.EventTime, reason: Int) {
player?.let { safePlayer ->
val manifest = safePlayer.currentManifest
if (manifest is HlsManifest) {
collector.onMainPlaylistTags(parseHlsSessionData(manifest.multivariantPlaylist.tags))
}
}
}

private fun parseHlsSessionData(hlsTags: List<String>): List<SessionTag> {
return filterHlsSessionTags(hlsTags)
.map { parseHlsSessionTag(it) }
.filter { it.key != null && it.key.contains(HLS_SESSION_LITIX_PREFIX) }
}

private fun filterHlsSessionTags(rawTags: List<String>) =
rawTags.filter { it.substring(1).startsWith("EXT-X-SESSION-DATA") }

private fun parseHlsSessionTag(line: String): SessionTag {
val dataId: Matcher = RX_SESSION_TAG_DATA_ID.matcher(line)
val value: Matcher = RX_SESSION_TAG_VALUES.matcher(line)
var parsedDataId: String? = ""
var parsedValue: String? = ""
if (dataId.find()) {
parsedDataId = dataId.group(1)?.replace(HLS_SESSION_LITIX_PREFIX, "")
} else {
MuxLogger.d(LOG_TAG, "Data-ID not found in session data: $line")
}
if (value.find()) {
parsedValue = value.group(1)
} else {
MuxLogger.d(LOG_TAG, "Value not found in session data: $line")
}
return SessionTag(parsedDataId, parsedValue)
}
}
}

/**
* Creates a listener that listens for timeline changes and updates HLS session data if we're on an
* HLS stream.
* This class should only be instantiated if ExoPlayer's HLS extension is available at runtime
* @see [.isHlsExtensionAvailable]
*/
@JvmSynthetic
internal fun createExoSessionDataBinding(): MuxPlayerAdapter.PlayerBinding<ExoPlayer> =
SessionDataPlayerBinding()
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.mux.stats.sdk.muxstats.internal

import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.hls.HlsManifest
import com.mux.stats.sdk.core.util.MuxLogger

// lazily-cached check for the HLS extension, which may not be available at runtime
@OptIn(UnstableApi::class) // opting-in to HlsManifest
private val hlsExtensionAvailable: Boolean by lazy {
try {
Class.forName(HlsManifest::class.java.canonicalName!!)
true
} catch (e: ClassNotFoundException) {
MuxLogger.w("isHlsExtensionAvailable", "HLS extension not found. Some features may not work")
false
}
}

@JvmSynthetic
internal fun isHlsExtensionAvailable() = hlsExtensionAvailable
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ class MuxStatsSdkMedia3<P : Player> @JvmOverloads constructor(
customerData: CustomerData,
player: P,
playerView: View? = null,
playerBinding: MuxPlayerAdapter.PlayerBinding<P> = BaseMedia3Binding(),
customOptions: CustomOptions? = null,
playerBinding: MuxPlayerAdapter.PlayerBinding<P>,
) : MuxDataSdk<P, View>(
context = context,
envKey = envKey,
Expand Down

0 comments on commit 94496c3

Please sign in to comment.