Skip to content

Commit

Permalink
feat: Add Livestream Latency Metrics (#15)
Browse files Browse the repository at this point in the history
* Add some methods that parse tags

* Add livestream metrics

* Add example
  • Loading branch information
daytime-em authored Jun 30, 2023
1 parent 741da4d commit 8e19a65
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,19 @@ class MainActivity : AppCompatActivity() {
destination = Intent(this, ImaAdsActivity::class.java)
),
Example(
title = "Background playback",
destination = Intent(this, BackgroundPlayActivity::class.java)
title = "Live Playback",
destination = Intent(this, BasicPlayerActivity::class.java).apply {
putExtra(
BasicPlayerActivity.EXTRA_URL,
"https://stream.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM.m3u8"
)
}
),
// TODO: post-beta, add APIs for talking to a `MediaSessionService`
// Example(
// title = "Background playback",
// destination = Intent(this, BackgroundPlayActivity::class.java)
// ),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ class BasicPlayerActivity : AppCompatActivity() {

override fun onResume() {
super.onResume()
startPlaying(Constants.VOD_TEST_URL_DRAGON_WARRIOR_LADY)
startPlaying(
intent.getStringExtra(EXTRA_URL) ?: Constants.VOD_TEST_URL_DRAGON_WARRIOR_LADY
)
}

override fun onPause() {
Expand Down Expand Up @@ -92,9 +94,14 @@ class BasicPlayerActivity : AppCompatActivity() {
addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
Log.e(javaClass.simpleName, "player error!", error)
Toast.makeText(this@BasicPlayerActivity, error.localizedMessage, Toast.LENGTH_SHORT).show()
Toast.makeText(this@BasicPlayerActivity, error.localizedMessage, Toast.LENGTH_SHORT)
.show()
}
})
}
}

companion object {
const val EXTRA_URL: String = "com.mux.video.url"
}
}
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ allprojects {
project.ext {
media3Version = '1.0.2'
// coreVersion = "0.7.4"
coreVersion = 'dev-releases-v0.7.5-aab9aec'
coreVersion = 'dev-feat-live-069a758'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.mux.stats.sdk.core.util.MuxLogger
import com.mux.stats.sdk.muxstats.bandwidth.BandwidthMetricDispatcher
import com.mux.stats.sdk.muxstats.bandwidth.TrackedHeader
import com.mux.stats.sdk.muxstats.internal.createExoSessionDataBinding
import com.mux.stats.sdk.muxstats.internal.populateLiveStreamData
import com.mux.stats.sdk.muxstats.internal.createErrorDataBinding
import java.io.IOException
import java.util.regex.Pattern
Expand Down Expand Up @@ -96,6 +97,7 @@ private class MuxAnalyticsListener(
eventTime.timeline.takeIf { it.windowCount > 0 }?.let { tl ->
val window = Timeline.Window().apply { tl.getWindow(0, this) }
collector.sourceDurationMs = window.durationMs
collector.populateLiveStreamData(window)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.mux.stats.sdk.muxstats.internal

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

/*
* HlsUtils.kt: Utility functions for working with HLS playlists in exoplayer
*/

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

/**
* True when Exoplayer's HLS extension is available at runtime
*/
@JvmSynthetic
internal fun isHlsExtensionAvailable() = hlsExtensionAvailable

/**
* Add livestream data to a [MuxStateCollector] if the given [Window] represents a live stream
*/
@JvmSynthetic
internal fun MuxStateCollector.populateLiveStreamData(window: Window) {
if (window.isLive()) {
hlsManifestNewestTime = window.windowStartTimeMs
hlsHoldBack = parseManifestTagL(window, "HOLD-BACK")
hlsPartHoldBack = parseManifestTagL(window, "PART-HOLD-BACK")
hlsPartTargetDuration = parseManifestTagL(window, "PART-TARGET")
hlsTargetDuration = parseManifestTagL(window, "EXT-X-TARGETDURATION")
}
}

/**
* Parses manifest tags representing a named numerical value, returning the value as a Long
*/
@JvmSynthetic
internal fun parseManifestTagL(currentWindow: Window, tagName: String): Long {
var value: String = parseManifestTag(currentWindow, tagName)
value = value.replace(".", "")
try {
return value.toLong()
} catch (e: NumberFormatException) {
MuxLogger.exception(e, "Manifest Parsing", "Bad number format for value: $value")
}
return -1L
}

/**
* Parses manifest tags representing a named numerical value, returning the value as a string
*/
@OptIn(UnstableApi::class)
@JvmSynthetic
internal fun parseManifestTag(currentWindow: Timeline.Window, tagName: String): String {
if (!isHlsExtensionAvailable()) {
return "-1"
}

if (currentWindow.manifest != null && tagName.isNotEmpty()) {
if (currentWindow.manifest is HlsManifest) {
val manifest = currentWindow.manifest as HlsManifest
for (tag in manifest.mediaPlaylist.tags) {
if (tag.contains(tagName)) {
var value = tag.split(tagName).toTypedArray()[1]
if (value.contains(",")) {
value = value.split(",").toTypedArray()[0]
}
if (value.startsWith("=") || value.startsWith(":")) {
value = value.substring(1, value.length)
}
return value
}
}
}
}
return "-1"
}

This file was deleted.

0 comments on commit 8e19a65

Please sign in to comment.