Skip to content

Commit

Permalink
v2.5.9
Browse files Browse the repository at this point in the history
* Add Support for Measuring "glass-to-glass" live latency for HLS and DASH (#181)

Co-authored-by: Tomislav Kordic <32546640+tomkordic@users.noreply.github.com>
  • Loading branch information
daytime-em and tomkordic committed Dec 20, 2021
1 parent 35eba9d commit e02e2bd
Show file tree
Hide file tree
Showing 11 changed files with 517 additions and 30 deletions.
4 changes: 2 additions & 2 deletions MuxExoPlayer/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ android {
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
versionCode 35
versionCode 36
versionName project.ext.versionName
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down Expand Up @@ -196,7 +196,7 @@ dependencies {
compileOnly 'com.google.android.gms:play-services-ads:15.0.1'
compileOnly 'com.google.android.gms:play-services-ads-identifier:15.0.1'

def muxCoreImport = 'com.mux:stats.muxcore:7.0.5'
def muxCoreImport = 'com.mux:stats.muxcore:7.0.7'

debugApi muxCoreImport
releaseEmbed muxCoreImport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.mux.stats.sdk.core.CustomOptions;
Expand Down Expand Up @@ -73,7 +75,7 @@
* of {@link ExoPlayer} version and make sure that all events are passed to {@link MuxStats} in
* the correct order and with appropriate statistics.
*/
public class MuxBaseExoPlayer extends EventBus implements IPlayerListener {
public abstract class MuxBaseExoPlayer extends EventBus implements IPlayerListener {

protected static final String TAG = "MuxStatsListener";

Expand Down Expand Up @@ -117,6 +119,8 @@ public class MuxBaseExoPlayer extends EventBus implements IPlayerListener {
* Media container and can also be different from actual Media duration.
*/
protected Long sourceDuration;
/** Store all time detailes of current segment */
protected Timeline.Window currentTimelineWindow = new Window();
/** This is used to update the current playback position in real time. */
protected ExoPlayerHandler playerHandler;
protected Timer updatePlayheadPositionTimer;
Expand Down Expand Up @@ -326,6 +330,10 @@ public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
}
}

protected abstract boolean isLivePlayback();

protected abstract String parseHlsManifestTag(String tagName);

/**
* Allow HTTP headers with a given name to be passed to the backend. By default we ignore all HTTP
* headers that are not in the {@link BandwidthMetricDispatcher#allowedHeaders} list.
Expand Down Expand Up @@ -616,9 +624,7 @@ public PlayerState getState() {
}

/**
* Detect if the current media item being played contains a video track. If it does
* it will initialize the {@link #frameRenderedListener} or if it does not then it will initialize
* {@link #updatePlayheadPositionTimer}.
* Detect if the current media item being played contains a video track.
*/
protected void configurePlaybackHeadUpdateInterval() {
if (player == null || player.get() == null) {
Expand All @@ -643,9 +649,7 @@ protected void configurePlaybackHeadUpdateInterval() {
}

/**
* Start a periodic timer which will update the playback position on every 150 ms. We only use
* this method in case the media item has no video component, in case it does the
* {@link #frameRenderedListener} will be updating the playback position.
* Start a periodic timer which will update the playback position on every 150 ms.
*/
protected void setPlaybackHeadUpdateInterval() {
if (updatePlayheadPositionTimer != null) {
Expand Down Expand Up @@ -710,6 +714,78 @@ public int getPlayerViewHeight() {
return 0;
}

/**
* This is the time of the current playback position as extrapolated from the PDT tags in the
* stream. Only available for DASH and HLS.
*
* @return time.
*/
@Override
public Long getPlayerProgramTime() {
if (currentTimelineWindow != null && playerHandler != null) {
return currentTimelineWindow.windowStartTimeMs + playerHandler.getPlayerCurrentPosition();
}
return -1L;
}

/**
* This is the time of the furthest position in the manifest as extrapolated from the PDT tags in
* the stream. Only available for DASH and HLS.
*
* @return time.
*/
@Override
public Long getPlayerManifestNewestTime() {
if (currentTimelineWindow != null && isLivePlayback()) {
return currentTimelineWindow.windowStartTimeMs;
}
return -1L;
}

/**
* The configured holdback value for a live stream (ms). Analagous to the HOLD-BACK manifest tag.
* Only available for DASH and HLS.
*
* @return value in milliseconds.
*/
@Override
public Long getVideoHoldback() {
return parseHlsManifestTagLong("HOLD-BACK");
}

/**
* The configured holdback value for parts in a low latency live stream (ms). Analagous to the
* PART-HOLD-BACK manfiest tag. Only available for DASH and HLS.
*
* @return value in milliseconds.
*/
@Override
public Long getVideoPartHoldback() {
return parseHlsManifestTagLong("PART-HOLD-BACK");
}

/**
* The configured target duration for parts in a low-latency live stream (ms). Analogous to the
* PART-TARGET attribute within the EXT-X-PART-INF manifest tag. Only available for DASH and HLS.
*
* @return value in milliseconds.
*/
@Override
public Long getVideoPartTargetDuration() {
return parseHlsManifestTagLong("PART-TARGET");
}

/**
* The configured target duration for segments in a live stream (ms). Analogous to the
* EXT-X-TARGETDURATION manifest tag. Only available for DASH and HLS.
*
* @return value in milliseconds.
*/
@Override
public Long getVideoTargetDuration() {
return parseHlsManifestTagLong("EXT-X-TARGETDURATION");
}

/**
* Determine if the current player state is paused.
*
Expand Down Expand Up @@ -927,6 +1003,23 @@ private void resetInternalStats() {
numberOfEventsSent = 0;
firstFrameReceived = false;
firstFrameRenderedAt = -1;
currentTimelineWindow = new Window();
}

/**
* See {{@link #parseHlsManifestTagLong(String)}}, parse the tag value as a Long value.
* @param tagName tag name to parse
* @return Long value of the tag if possible, -1 otherwise.
*/
private Long parseHlsManifestTagLong(String tagName) {
String value = parseHlsManifestTag(tagName);
value = value.replace(".", "");
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
MuxLogger.d(TAG, "Bad number format for value: " + value);
}
return -1L;
}

/**
Expand Down Expand Up @@ -1144,29 +1237,29 @@ public long getElapsedRealtime() {
return elapsedRealtime();
}

@Override
public void outputLog(LogPriority logPriority, String tag, String msg) {
switch (logPriority) {
case ERROR:
Log.e(tag, msg);
break;
case WARN:
Log.w(tag, msg);
break;
case INFO:
Log.i(tag, msg);
break;
case DEBUG:
Log.d(tag, msg);
break;
case VERBOSE:
default: // fall-through
Log.v(tag, msg);
break;
}
@Override
public void outputLog(LogPriority logPriority, String tag, String msg) {
switch (logPriority) {
case ERROR:
Log.e(tag, msg);
break;
case WARN:
Log.w(tag, msg);
break;
case INFO:
Log.i(tag, msg);
break;
case DEBUG:
Log.d(tag, msg);
break;
case VERBOSE:
default: // fall-through
Log.v(tag, msg);
break;
}
}

/**
/**
* Print underlying {@link MuxStats} SDK messages on the logcat. This will only be
* called if {@link #enableMuxCoreDebug(boolean, boolean)} is called with first argument as true
*
Expand Down Expand Up @@ -1238,6 +1331,17 @@ public BandwidthMetricData onLoadCanceled(String segmentUrl) {
protected BandwidthMetricData onLoad(long mediaStartTimeMs, long mediaEndTimeMs,
String segmentUrl, int dataType, String host, String segmentMimeType
) {
// Populate segment time details.
if (player != null && player.get() != null) {
synchronized (currentTimelineWindow) {
try {
player.get().getCurrentTimeline()
.getWindow(player.get().getCurrentWindowIndex(), currentTimelineWindow);
} catch (Exception e) {
// Failed to obtrain data, ignore, we will get it on next call
}
}
}
BandwidthMetricData segmentData = new BandwidthMetricData();
// TODO RequestStart timestamp is currently not available from ExoPlayer
segmentData.setRequestResponseStart(System.currentTimeMillis());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.hls.HlsManifest;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.mux.stats.sdk.core.CustomOptions;
import com.mux.stats.sdk.core.model.CustomerData;
Expand Down Expand Up @@ -111,6 +112,25 @@ public MuxStatsExoPlayer(Context ctx, ExoPlayer player, String playerName,
}
}

/**
* Extracts the tag value from live HLS segment, returns -1 if it is not an HLS stream, not a live
* playback.
*
* NOT SUPPORTED FOR THIS VERSION OF ExoPlayer
*
* @param tagName name of the tag to extract from the HLS manifest.
* @return tag value if tag is found and we are playing HLS live stream, -1 string otherwise.
*/
@Override
protected String parseHlsManifestTag(String tagName) {
return "-1";
}

@Override
protected boolean isLivePlayback() {
return false;
}

@Override
public void release() {
if (player != null && this.player.get() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.hls.HlsManifest;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.mux.stats.sdk.core.CustomOptions;
import com.mux.stats.sdk.core.model.CustomerData;
Expand Down Expand Up @@ -111,6 +112,48 @@ public MuxStatsExoPlayer(Context ctx, ExoPlayer player, String playerName,
}
}

/**
* Extracts the tag value from live HLS segment, returns -1 if it is not an HLS stream, not a live
* playback.
*
* @param tagName name of the tag to extract from the HLS manifest.
* @return tag value if tag is found and we are playing HLS live stream, -1 string otherwise.
*/
@Override
protected String parseHlsManifestTag(String tagName) {
synchronized (currentTimelineWindow) {
if (currentTimelineWindow != null && currentTimelineWindow.manifest != null
&& isLivePlayback() && tagName != null && tagName.length() > 0) {
if (currentTimelineWindow.manifest instanceof HlsManifest) {
HlsManifest manifest = (HlsManifest) currentTimelineWindow.manifest;
if (manifest.mediaPlaylist.tags != null) {
for (String tag : manifest.mediaPlaylist.tags) {
if (tag.contains(tagName)) {
String value = tag.split(tagName)[1];
if (value.contains(",")) {
value = value.split(",")[0];
}
if (value.startsWith("=") || value.startsWith(":")) {
value = value.substring(1, value.length());
}
return value;
}
}
}
}
}
}
return "-1";
}

@Override
protected boolean isLivePlayback() {
if (currentTimelineWindow != null) {
return currentTimelineWindow.isLive;
}
return false;
}

@Override
public void release() {
if (player != null && this.player.get() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.hls.HlsManifest;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.mux.stats.sdk.core.CustomOptions;
import com.mux.stats.sdk.core.model.CustomerData;
Expand Down Expand Up @@ -112,6 +113,48 @@ public MuxStatsExoPlayer(Context ctx, ExoPlayer player, String playerName,
}
}

/**
* Extracts the tag value from live HLS segment, returns -1 if it is not an HLS stream, not a live
* playback.
*
* @param tagName name of the tag to extract from the HLS manifest.
* @return tag value if tag is found and we are playing HLS live stream, -1 string otherwise.
*/
@Override
protected String parseHlsManifestTag(String tagName) {
synchronized (currentTimelineWindow) {
if (currentTimelineWindow != null && currentTimelineWindow.manifest != null
&& isLivePlayback() && tagName != null && tagName.length() > 0) {
if (currentTimelineWindow.manifest instanceof HlsManifest) {
HlsManifest manifest = (HlsManifest) currentTimelineWindow.manifest;
if (manifest.mediaPlaylist.tags != null) {
for (String tag : manifest.mediaPlaylist.tags) {
if (tag.contains(tagName)) {
String value = tag.split(tagName)[1];
if (value.contains(",")) {
value = value.split(",")[0];
}
if (value.startsWith("=") || value.startsWith(":")) {
value = value.substring(1, value.length());
}
return value;
}
}
}
}
}
}
return "-1";
}

@Override
protected boolean isLivePlayback() {
if (currentTimelineWindow != null) {
return currentTimelineWindow.isLive;
}
return false;
}

@Override
public void release() {
if (this.player != null && this.player.get() != null) {
Expand Down
Loading

0 comments on commit e02e2bd

Please sign in to comment.