compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.5.0-alpha01'
Artifact androidx.media3:media3-exoplayer:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)
Creates a media metrics listener.
Returns the used by this listener.
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.analytics;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import android.annotation.SuppressLint;
import android.content.Context;
import android.media.DeniedByServerException;
import android.media.MediaCodec;
import android.media.MediaDrm;
import android.media.MediaDrmResetException;
import android.media.NotProvisionedException;
import android.media.metrics.LogSessionId;
import android.media.metrics.MediaMetricsManager;
import android.media.metrics.NetworkEvent;
import android.media.metrics.PlaybackErrorEvent;
import android.media.metrics.PlaybackMetrics;
import android.media.metrics.PlaybackSession;
import android.media.metrics.PlaybackStateEvent;
import android.media.metrics.TrackChangeEvent;
import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.OsConstants;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.C;
import androidx.media3.common.C.ContentType;
import androidx.media3.common.DrmInitData;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.ParserException;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.util.NetworkTypeObserver;
import androidx.media3.common.util.NullableType;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.FileDataSource;
import androidx.media3.datasource.HttpDataSource;
import androidx.media3.datasource.UdpDataSource;
import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.audio.AudioSink;
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager;
import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.drm.UnsupportedDrmException;
import androidx.media3.exoplayer.mediacodec.MediaCodecDecoderException;
import androidx.media3.exoplayer.mediacodec.MediaCodecRenderer;
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MediaLoadData;
import androidx.media3.exoplayer.source.MediaSource;
import com.google.common.collect.ImmutableList;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.UUID;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* An {@link AnalyticsListener} that interacts with the Android {@link MediaMetricsManager}.
*
* <p>It listens to playback events and forwards them to a {@link PlaybackSession}. The {@link
* LogSessionId} of the playback session can be obtained with {@link #getLogSessionId()}.
*/
@UnstableApi
@RequiresApi(31)
public final class MediaMetricsListener
implements AnalyticsListener, PlaybackSessionManager.Listener {
/**
* Creates a media metrics listener.
*
* @param context A context.
* @return The {@link MediaMetricsListener}, or null if the {@link Context#MEDIA_METRICS_SERVICE
* media metrics service} isn't available.
*/
@Nullable
public static MediaMetricsListener create(Context context) {
@Nullable
MediaMetricsManager mediaMetricsManager =
(MediaMetricsManager) context.getSystemService(Context.MEDIA_METRICS_SERVICE);
return mediaMetricsManager == null
? null
: new MediaMetricsListener(context, mediaMetricsManager.createPlaybackSession());
}
private final Context context;
private final PlaybackSessionManager sessionManager;
private final PlaybackSession playbackSession;
private final long startTimeMs;
private final Timeline.Window window;
private final Timeline.Period period;
private final HashMap<String, Long> bandwidthTimeMs;
private final HashMap<String, Long> bandwidthBytes;
@Nullable private String activeSessionId;
@Nullable private PlaybackMetrics.Builder metricsBuilder;
private @Player.DiscontinuityReason int discontinuityReason;
private int currentPlaybackState;
private int currentNetworkType;
@Nullable private PlaybackException pendingPlayerError;
@Nullable private PendingFormatUpdate pendingVideoFormat;
@Nullable private PendingFormatUpdate pendingAudioFormat;
@Nullable private PendingFormatUpdate pendingTextFormat;
@Nullable private Format currentVideoFormat;
@Nullable private Format currentAudioFormat;
@Nullable private Format currentTextFormat;
private boolean isSeeking;
private int ioErrorType;
private boolean hasFatalError;
private int droppedFrames;
private int playedFrames;
private int audioUnderruns;
private boolean reportedEventsForCurrentSession;
/**
* Creates the listener.
*
* @param context A {@link Context}.
*/
private MediaMetricsListener(Context context, PlaybackSession playbackSession) {
context = context.getApplicationContext();
this.context = context;
this.playbackSession = playbackSession;
window = new Timeline.Window();
period = new Timeline.Period();
bandwidthBytes = new HashMap<>();
bandwidthTimeMs = new HashMap<>();
startTimeMs = SystemClock.elapsedRealtime();
currentPlaybackState = PlaybackStateEvent.STATE_NOT_STARTED;
currentNetworkType = NetworkEvent.NETWORK_TYPE_UNKNOWN;
sessionManager = new DefaultPlaybackSessionManager();
sessionManager.setListener(this);
}
/** Returns the {@link LogSessionId} used by this listener. */
public LogSessionId getLogSessionId() {
return playbackSession.getSessionId();
}
// PlaybackSessionManager.Listener implementation.
@Override
public void onSessionCreated(EventTime eventTime, String sessionId) {}
@Override
public void onSessionActive(EventTime eventTime, String sessionId) {
if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) {
// Ignore ad sessions.
return;
}
finishCurrentSession();
activeSessionId = sessionId;
metricsBuilder =
new PlaybackMetrics.Builder()
.setPlayerName(MediaLibraryInfo.TAG)
.setPlayerVersion(MediaLibraryInfo.VERSION);
maybeUpdateTimelineMetadata(eventTime.timeline, eventTime.mediaPeriodId);
}
@Override
public void onAdPlaybackStarted(
EventTime eventTime, String contentSessionId, String adSessionId) {}
@Override
public void onSessionFinished(
EventTime eventTime, String sessionId, boolean automaticTransitionToNextPlayback) {
if ((eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd())
|| !sessionId.equals(activeSessionId)) {
// Ignore ad sessions and other sessions that are finished before becoming active.
} else {
finishCurrentSession();
}
bandwidthTimeMs.remove(sessionId);
bandwidthBytes.remove(sessionId);
}
// AnalyticsListener implementation.
@Override
public void onPositionDiscontinuity(
EventTime eventTime,
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
isSeeking = true;
}
discontinuityReason = reason;
}
@Override
public void onVideoDisabled(EventTime eventTime, DecoderCounters decoderCounters) {
// TODO(b/181122234): DecoderCounters are not re-reported at period boundaries.
droppedFrames += decoderCounters.droppedBufferCount;
playedFrames += decoderCounters.renderedOutputBufferCount;
}
@Override
public void onBandwidthEstimate(
EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
if (eventTime.mediaPeriodId != null) {
String sessionId =
sessionManager.getSessionForMediaPeriodId(
eventTime.timeline, checkNotNull(eventTime.mediaPeriodId));
@Nullable Long prevBandwidthBytes = bandwidthBytes.get(sessionId);
@Nullable Long prevBandwidthTimeMs = bandwidthTimeMs.get(sessionId);
bandwidthBytes.put(
sessionId, (prevBandwidthBytes == null ? 0 : prevBandwidthBytes) + totalBytesLoaded);
bandwidthTimeMs.put(
sessionId, (prevBandwidthTimeMs == null ? 0 : prevBandwidthTimeMs) + totalLoadTimeMs);
}
}
@Override
public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {
if (eventTime.mediaPeriodId == null) {
// This event arrived after the media has been removed from the playlist or a custom
// MediaSource forgot to set the right id. Ignore the track change in these cases.
return;
}
PendingFormatUpdate update =
new PendingFormatUpdate(
checkNotNull(mediaLoadData.trackFormat),
mediaLoadData.trackSelectionReason,
sessionManager.getSessionForMediaPeriodId(
eventTime.timeline, checkNotNull(eventTime.mediaPeriodId)));
switch (mediaLoadData.trackType) {
case C.TRACK_TYPE_VIDEO:
case C.TRACK_TYPE_DEFAULT:
pendingVideoFormat = update;
break;
case C.TRACK_TYPE_AUDIO:
pendingAudioFormat = update;
break;
case C.TRACK_TYPE_TEXT:
pendingTextFormat = update;
break;
default:
// Other track type. Ignore.
}
}
@Override
public void onVideoSizeChanged(EventTime eventTime, VideoSize videoSize) {
@Nullable PendingFormatUpdate pendingVideoFormat = this.pendingVideoFormat;
if (pendingVideoFormat != null && pendingVideoFormat.format.height == Format.NO_VALUE) {
Format formatWithHeightAndWidth =
pendingVideoFormat
.format
.buildUpon()
.setWidth(videoSize.width)
.setHeight(videoSize.height)
.build();
this.pendingVideoFormat =
new PendingFormatUpdate(
formatWithHeightAndWidth,
pendingVideoFormat.selectionReason,
pendingVideoFormat.sessionId);
}
}
@Override
public void onLoadError(
EventTime eventTime,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData,
IOException error,
boolean wasCanceled) {
ioErrorType = mediaLoadData.dataType;
}
@Override
public void onPlayerError(EventTime eventTime, PlaybackException error) {
pendingPlayerError = error;
}
@Override
public void onEvents(Player player, Events events) {
if (events.size() == 0) {
return;
}
maybeAddSessions(events);
long realtimeMs = SystemClock.elapsedRealtime();
maybeUpdateMetricsBuilderValues(player, events);
maybeReportPlaybackError(realtimeMs);
maybeReportTrackChanges(player, events, realtimeMs);
maybeReportNetworkChange(realtimeMs);
maybeReportPlaybackStateChange(player, events, realtimeMs);
if (events.contains(AnalyticsListener.EVENT_PLAYER_RELEASED)) {
sessionManager.finishAllSessions(events.getEventTime(EVENT_PLAYER_RELEASED));
}
}
private void maybeAddSessions(Events events) {
for (int i = 0; i < events.size(); i++) {
@EventFlags int event = events.get(i);
EventTime eventTime = events.getEventTime(event);
if (event == EVENT_TIMELINE_CHANGED) {
sessionManager.updateSessionsWithTimelineChange(eventTime);
} else if (event == EVENT_POSITION_DISCONTINUITY) {
sessionManager.updateSessionsWithDiscontinuity(eventTime, discontinuityReason);
} else {
sessionManager.updateSessions(eventTime);
}
}
}
private void maybeUpdateMetricsBuilderValues(Player player, Events events) {
if (events.contains(EVENT_TIMELINE_CHANGED)) {
EventTime eventTime = events.getEventTime(EVENT_TIMELINE_CHANGED);
if (metricsBuilder != null) {
maybeUpdateTimelineMetadata(eventTime.timeline, eventTime.mediaPeriodId);
}
}
if (events.contains(EVENT_TRACKS_CHANGED) && metricsBuilder != null) {
@Nullable DrmInitData drmInitData = getDrmInitData(player.getCurrentTracks().getGroups());
if (drmInitData != null) {
castNonNull(metricsBuilder).setDrmType(getDrmType(drmInitData));
}
}
if (events.contains(EVENT_AUDIO_UNDERRUN)) {
audioUnderruns++;
}
}
private void maybeReportPlaybackError(long realtimeMs) {
@Nullable PlaybackException error = pendingPlayerError;
if (error == null) {
return;
}
ErrorInfo errorInfo =
getErrorInfo(
error, context, /* lastIoErrorForManifest= */ ioErrorType == C.DATA_TYPE_MANIFEST);
playbackSession.reportPlaybackErrorEvent(
new PlaybackErrorEvent.Builder()
.setTimeSinceCreatedMillis(realtimeMs - startTimeMs)
.setErrorCode(errorInfo.errorCode)
.setSubErrorCode(errorInfo.subErrorCode)
.setException(error)
.build());
reportedEventsForCurrentSession = true;
pendingPlayerError = null;
}
private void maybeReportTrackChanges(Player player, Events events, long realtimeMs) {
if (events.contains(EVENT_TRACKS_CHANGED)) {
Tracks tracks = player.getCurrentTracks();
boolean isVideoSelected = tracks.isTypeSelected(C.TRACK_TYPE_VIDEO);
boolean isAudioSelected = tracks.isTypeSelected(C.TRACK_TYPE_AUDIO);
boolean isTextSelected = tracks.isTypeSelected(C.TRACK_TYPE_TEXT);
if (isVideoSelected || isAudioSelected || isTextSelected) {
// Ignore updates with insufficient information where no tracks are selected.
if (!isVideoSelected) {
maybeUpdateVideoFormat(realtimeMs, /* videoFormat= */ null, C.SELECTION_REASON_UNKNOWN);
}
if (!isAudioSelected) {
maybeUpdateAudioFormat(realtimeMs, /* audioFormat= */ null, C.SELECTION_REASON_UNKNOWN);
}
if (!isTextSelected) {
maybeUpdateTextFormat(realtimeMs, /* textFormat= */ null, C.SELECTION_REASON_UNKNOWN);
}
}
}
if (canReportPendingFormatUpdate(pendingVideoFormat)
&& pendingVideoFormat.format.height != Format.NO_VALUE) {
maybeUpdateVideoFormat(
realtimeMs, pendingVideoFormat.format, pendingVideoFormat.selectionReason);
pendingVideoFormat = null;
}
if (canReportPendingFormatUpdate(pendingAudioFormat)) {
maybeUpdateAudioFormat(
realtimeMs, pendingAudioFormat.format, pendingAudioFormat.selectionReason);
pendingAudioFormat = null;
}
if (canReportPendingFormatUpdate(pendingTextFormat)) {
maybeUpdateTextFormat(
realtimeMs, pendingTextFormat.format, pendingTextFormat.selectionReason);
pendingTextFormat = null;
}
}
@EnsuresNonNullIf(result = true, expression = "#1")
private boolean canReportPendingFormatUpdate(@Nullable PendingFormatUpdate pendingFormatUpdate) {
return pendingFormatUpdate != null
&& pendingFormatUpdate.sessionId.equals(sessionManager.getActiveSessionId());
}
private void maybeReportNetworkChange(long realtimeMs) {
int networkType = getNetworkType(context);
if (networkType != currentNetworkType) {
currentNetworkType = networkType;
playbackSession.reportNetworkEvent(
new NetworkEvent.Builder()
.setNetworkType(networkType)
.setTimeSinceCreatedMillis(realtimeMs - startTimeMs)
.build());
}
}
private void maybeReportPlaybackStateChange(Player player, Events events, long realtimeMs) {
if (player.getPlaybackState() != Player.STATE_BUFFERING) {
isSeeking = false;
}
if (player.getPlayerError() == null) {
hasFatalError = false;
} else if (events.contains(EVENT_PLAYER_ERROR)) {
hasFatalError = true;
}
int newPlaybackState = resolveNewPlaybackState(player);
if (currentPlaybackState != newPlaybackState) {
currentPlaybackState = newPlaybackState;
reportedEventsForCurrentSession = true;
playbackSession.reportPlaybackStateEvent(
new PlaybackStateEvent.Builder()
.setState(currentPlaybackState)
.setTimeSinceCreatedMillis(realtimeMs - startTimeMs)
.build());
}
}
private int resolveNewPlaybackState(Player player) {
@Player.State int playerPlaybackState = player.getPlaybackState();
if (isSeeking) {
// Seeking takes precedence over errors such that we report a seek while in error state.
return PlaybackStateEvent.STATE_SEEKING;
} else if (hasFatalError) {
return PlaybackStateEvent.STATE_FAILED;
} else if (playerPlaybackState == Player.STATE_ENDED) {
return PlaybackStateEvent.STATE_ENDED;
} else if (playerPlaybackState == Player.STATE_BUFFERING) {
if (currentPlaybackState == PlaybackStateEvent.STATE_NOT_STARTED
|| currentPlaybackState == PlaybackStateEvent.STATE_JOINING_FOREGROUND
|| currentPlaybackState == PlaybackStateEvent.STATE_STOPPED) {
return PlaybackStateEvent.STATE_JOINING_FOREGROUND;
}
if (!player.getPlayWhenReady()) {
return PlaybackStateEvent.STATE_PAUSED_BUFFERING;
}
return player.getPlaybackSuppressionReason() != Player.PLAYBACK_SUPPRESSION_REASON_NONE
? PlaybackStateEvent.STATE_SUPPRESSED_BUFFERING
: PlaybackStateEvent.STATE_BUFFERING;
} else if (playerPlaybackState == Player.STATE_READY) {
if (!player.getPlayWhenReady()) {
return PlaybackStateEvent.STATE_PAUSED;
}
return player.getPlaybackSuppressionReason() != Player.PLAYBACK_SUPPRESSION_REASON_NONE
? PlaybackStateEvent.STATE_SUPPRESSED
: PlaybackStateEvent.STATE_PLAYING;
} else if (playerPlaybackState == Player.STATE_IDLE
&& currentPlaybackState != PlaybackStateEvent.STATE_NOT_STARTED) {
// This case only applies for calls to player.stop(). All other IDLE cases are handled by
// !isForeground, hasFatalError or isSuspended. NOT_STARTED is deliberately ignored.
return PlaybackStateEvent.STATE_STOPPED;
}
return currentPlaybackState;
}
private void maybeUpdateVideoFormat(
long realtimeMs, @Nullable Format videoFormat, @C.SelectionReason int trackSelectionReason) {
if (Util.areEqual(currentVideoFormat, videoFormat)) {
return;
}
if (currentVideoFormat == null && trackSelectionReason == C.SELECTION_REASON_UNKNOWN) {
trackSelectionReason = C.SELECTION_REASON_INITIAL;
}
currentVideoFormat = videoFormat;
reportTrackChangeEvent(
TrackChangeEvent.TRACK_TYPE_VIDEO, realtimeMs, videoFormat, trackSelectionReason);
}
private void maybeUpdateAudioFormat(
long realtimeMs, @Nullable Format audioFormat, @C.SelectionReason int trackSelectionReason) {
if (Util.areEqual(currentAudioFormat, audioFormat)) {
return;
}
if (currentAudioFormat == null && trackSelectionReason == C.SELECTION_REASON_UNKNOWN) {
trackSelectionReason = C.SELECTION_REASON_INITIAL;
}
currentAudioFormat = audioFormat;
reportTrackChangeEvent(
TrackChangeEvent.TRACK_TYPE_AUDIO, realtimeMs, audioFormat, trackSelectionReason);
}
private void maybeUpdateTextFormat(
long realtimeMs, @Nullable Format textFormat, @C.SelectionReason int trackSelectionReason) {
if (Util.areEqual(currentTextFormat, textFormat)) {
return;
}
if (currentTextFormat == null && trackSelectionReason == C.SELECTION_REASON_UNKNOWN) {
trackSelectionReason = C.SELECTION_REASON_INITIAL;
}
currentTextFormat = textFormat;
reportTrackChangeEvent(
TrackChangeEvent.TRACK_TYPE_TEXT, realtimeMs, textFormat, trackSelectionReason);
}
private void reportTrackChangeEvent(
int type,
long realtimeMs,
@Nullable Format format,
@C.SelectionReason int trackSelectionReason) {
TrackChangeEvent.Builder builder =
new TrackChangeEvent.Builder(type).setTimeSinceCreatedMillis(realtimeMs - startTimeMs);
if (format != null) {
builder.setTrackState(TrackChangeEvent.TRACK_STATE_ON);
builder.setTrackChangeReason(getTrackChangeReason(trackSelectionReason));
if (format.containerMimeType != null) {
// TODO(b/181121074): Progressive container MIME type is not filled in by MediaSource.
builder.setContainerMimeType(format.containerMimeType);
}
if (format.sampleMimeType != null) {
builder.setSampleMimeType(format.sampleMimeType);
}
if (format.codecs != null) {
builder.setCodecName(format.codecs);
}
if (format.bitrate != Format.NO_VALUE) {
builder.setBitrate(format.bitrate);
}
if (format.width != Format.NO_VALUE) {
builder.setWidth(format.width);
}
if (format.height != Format.NO_VALUE) {
builder.setHeight(format.height);
}
if (format.channelCount != Format.NO_VALUE) {
builder.setChannelCount(format.channelCount);
}
if (format.sampleRate != Format.NO_VALUE) {
builder.setAudioSampleRate(format.sampleRate);
}
if (format.language != null) {
Pair<String, @NullableType String> languageAndRegion =
getLanguageAndRegion(format.language);
builder.setLanguage(languageAndRegion.first);
if (languageAndRegion.second != null) {
builder.setLanguageRegion(languageAndRegion.second);
}
}
if (format.frameRate != Format.NO_VALUE) {
builder.setVideoFrameRate(format.frameRate);
}
} else {
builder.setTrackState(TrackChangeEvent.TRACK_STATE_OFF);
}
reportedEventsForCurrentSession = true;
playbackSession.reportTrackChangeEvent(builder.build());
}
@RequiresNonNull("metricsBuilder")
private void maybeUpdateTimelineMetadata(
Timeline timeline, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
PlaybackMetrics.Builder metricsBuilder = this.metricsBuilder;
if (mediaPeriodId == null) {
return;
}
int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid);
if (periodIndex == C.INDEX_UNSET) {
return;
}
timeline.getPeriod(periodIndex, period);
timeline.getWindow(period.windowIndex, window);
metricsBuilder.setStreamType(getStreamType(window.mediaItem));
if (window.durationUs != C.TIME_UNSET
&& !window.isPlaceholder
&& !window.isDynamic
&& !window.isLive()) {
metricsBuilder.setMediaDurationMillis(window.getDurationMs());
}
metricsBuilder.setPlaybackType(
window.isLive() ? PlaybackMetrics.PLAYBACK_TYPE_LIVE : PlaybackMetrics.PLAYBACK_TYPE_VOD);
reportedEventsForCurrentSession = true;
}
private void finishCurrentSession() {
if (metricsBuilder != null && reportedEventsForCurrentSession) {
metricsBuilder.setAudioUnderrunCount(audioUnderruns);
metricsBuilder.setVideoFramesDropped(droppedFrames);
metricsBuilder.setVideoFramesPlayed(playedFrames);
@Nullable Long networkTimeMs = bandwidthTimeMs.get(activeSessionId);
metricsBuilder.setNetworkTransferDurationMillis(networkTimeMs == null ? 0 : networkTimeMs);
// TODO(b/181121847): Report localBytesRead. This requires additional callbacks or plumbing.
@Nullable Long networkBytes = bandwidthBytes.get(activeSessionId);
metricsBuilder.setNetworkBytesRead(networkBytes == null ? 0 : networkBytes);
// TODO(b/181121847): Detect stream sources mixed and local depending on localBytesRead.
metricsBuilder.setStreamSource(
networkBytes != null && networkBytes > 0
? PlaybackMetrics.STREAM_SOURCE_NETWORK
: PlaybackMetrics.STREAM_SOURCE_UNKNOWN);
playbackSession.reportPlaybackMetrics(metricsBuilder.build());
}
metricsBuilder = null;
activeSessionId = null;
audioUnderruns = 0;
droppedFrames = 0;
playedFrames = 0;
currentVideoFormat = null;
currentAudioFormat = null;
currentTextFormat = null;
reportedEventsForCurrentSession = false;
}
private static int getTrackChangeReason(@C.SelectionReason int trackSelectionReason) {
switch (trackSelectionReason) {
case C.SELECTION_REASON_INITIAL:
return TrackChangeEvent.TRACK_CHANGE_REASON_INITIAL;
case C.SELECTION_REASON_ADAPTIVE:
return TrackChangeEvent.TRACK_CHANGE_REASON_ADAPTIVE;
case C.SELECTION_REASON_MANUAL:
return TrackChangeEvent.TRACK_CHANGE_REASON_MANUAL;
case C.SELECTION_REASON_TRICK_PLAY:
case C.SELECTION_REASON_UNKNOWN:
default:
return TrackChangeEvent.TRACK_CHANGE_REASON_OTHER;
}
}
private static Pair<String, @NullableType String> getLanguageAndRegion(String languageCode) {
String[] parts = Util.split(languageCode, "-");
return Pair.create(parts[0], parts.length >= 2 ? parts[1] : null);
}
private static int getNetworkType(Context context) {
switch (NetworkTypeObserver.getInstance(context).getNetworkType()) {
case C.NETWORK_TYPE_WIFI:
return NetworkEvent.NETWORK_TYPE_WIFI;
case C.NETWORK_TYPE_2G:
return NetworkEvent.NETWORK_TYPE_2G;
case C.NETWORK_TYPE_3G:
return NetworkEvent.NETWORK_TYPE_3G;
case C.NETWORK_TYPE_4G:
return NetworkEvent.NETWORK_TYPE_4G;
case C.NETWORK_TYPE_5G_SA:
return NetworkEvent.NETWORK_TYPE_5G_SA;
case C.NETWORK_TYPE_5G_NSA:
return NetworkEvent.NETWORK_TYPE_5G_NSA;
case C.NETWORK_TYPE_ETHERNET:
return NetworkEvent.NETWORK_TYPE_ETHERNET;
case C.NETWORK_TYPE_OFFLINE:
return NetworkEvent.NETWORK_TYPE_OFFLINE;
case C.NETWORK_TYPE_UNKNOWN:
return NetworkEvent.NETWORK_TYPE_UNKNOWN;
default:
return NetworkEvent.NETWORK_TYPE_OTHER;
}
}
private static int getStreamType(MediaItem mediaItem) {
if (mediaItem.localConfiguration == null) {
return PlaybackMetrics.STREAM_TYPE_UNKNOWN;
}
@ContentType
int contentType =
Util.inferContentTypeForUriAndMimeType(
mediaItem.localConfiguration.uri, mediaItem.localConfiguration.mimeType);
switch (contentType) {
case C.CONTENT_TYPE_HLS:
return PlaybackMetrics.STREAM_TYPE_HLS;
case C.CONTENT_TYPE_DASH:
return PlaybackMetrics.STREAM_TYPE_DASH;
case C.CONTENT_TYPE_SS:
return PlaybackMetrics.STREAM_TYPE_SS;
case C.CONTENT_TYPE_RTSP:
default:
return PlaybackMetrics.STREAM_TYPE_OTHER;
}
}
private static ErrorInfo getErrorInfo(
PlaybackException error, Context context, boolean lastIoErrorForManifest) {
if (error.errorCode == PlaybackException.ERROR_CODE_REMOTE_ERROR) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_PLAYER_REMOTE, /* subErrorCode= */ 0);
}
// Unpack the PlaybackException.
// TODO(b/190203080): Use error codes instead of the Exception's cause where possible.
boolean isRendererExoPlaybackException = false;
int rendererFormatSupport = C.FORMAT_UNSUPPORTED_TYPE;
if (error instanceof ExoPlaybackException) {
ExoPlaybackException exoPlaybackException = (ExoPlaybackException) error;
isRendererExoPlaybackException =
exoPlaybackException.type == ExoPlaybackException.TYPE_RENDERER;
rendererFormatSupport = exoPlaybackException.rendererFormatSupport;
}
Throwable cause = checkNotNull(error.getCause());
if (cause instanceof IOException) {
if (cause instanceof HttpDataSource.InvalidResponseCodeException) {
int responseCode = ((HttpDataSource.InvalidResponseCodeException) cause).responseCode;
return new ErrorInfo(
PlaybackErrorEvent.ERROR_IO_BAD_HTTP_STATUS, /* subErrorCode= */ responseCode);
} else if (cause instanceof HttpDataSource.InvalidContentTypeException
|| cause instanceof ParserException) {
return new ErrorInfo(
lastIoErrorForManifest
? PlaybackErrorEvent.ERROR_PARSING_MANIFEST_MALFORMED
: PlaybackErrorEvent.ERROR_PARSING_CONTAINER_MALFORMED,
/* subErrorCode= */ 0);
} else if (cause instanceof HttpDataSource.HttpDataSourceException
|| cause instanceof UdpDataSource.UdpDataSourceException) {
if (NetworkTypeObserver.getInstance(context).getNetworkType() == C.NETWORK_TYPE_OFFLINE) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_IO_NETWORK_UNAVAILABLE, /* subErrorCode= */ 0);
} else {
@Nullable Throwable detailedCause = cause.getCause();
if (detailedCause instanceof UnknownHostException) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_IO_DNS_FAILED, /* subErrorCode= */ 0);
} else if (detailedCause instanceof SocketTimeoutException) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_IO_CONNECTION_TIMEOUT, /* subErrorCode= */ 0);
} else if (cause instanceof HttpDataSource.HttpDataSourceException
&& ((HttpDataSource.HttpDataSourceException) cause).type
== HttpDataSource.HttpDataSourceException.TYPE_OPEN) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_IO_NETWORK_CONNECTION_FAILED, /* subErrorCode= */ 0);
} else {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_IO_CONNECTION_CLOSED, /* subErrorCode= */ 0);
}
}
} else if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_PLAYER_BEHIND_LIVE_WINDOW, /* subErrorCode= */ 0);
} else if (cause instanceof DrmSession.DrmSessionException) {
// Unpack DrmSessionException.
cause = checkNotNull(cause.getCause());
if (cause instanceof MediaDrm.MediaDrmStateException) {
String diagnosticsInfo = ((MediaDrm.MediaDrmStateException) cause).getDiagnosticInfo();
int subErrorCode = Util.getErrorCodeFromPlatformDiagnosticsInfo(diagnosticsInfo);
int errorCode = getDrmErrorCode(subErrorCode);
return new ErrorInfo(errorCode, subErrorCode);
} else if (Util.SDK_INT >= 23 && cause instanceof MediaDrmResetException) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_SYSTEM_ERROR, /* subErrorCode= */ 0);
} else if (cause instanceof NotProvisionedException) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_DRM_PROVISIONING_FAILED, /* subErrorCode= */ 0);
} else if (cause instanceof DeniedByServerException) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_DEVICE_REVOKED, /* subErrorCode= */ 0);
} else if (cause instanceof UnsupportedDrmException) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_DRM_SCHEME_UNSUPPORTED, /* subErrorCode= */ 0);
} else if (cause instanceof DefaultDrmSessionManager.MissingSchemeDataException) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_CONTENT_ERROR, /* subErrorCode= */ 0);
} else {
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_OTHER, /* subErrorCode= */ 0);
}
} else if (cause instanceof FileDataSource.FileDataSourceException
&& cause.getCause() instanceof FileNotFoundException) {
@Nullable Throwable notFoundCause = checkNotNull(cause.getCause()).getCause();
if (notFoundCause instanceof ErrnoException
&& ((ErrnoException) notFoundCause).errno == OsConstants.EACCES) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_IO_NO_PERMISSION, /* subErrorCode= */ 0);
} else {
return new ErrorInfo(PlaybackErrorEvent.ERROR_IO_FILE_NOT_FOUND, /* subErrorCode= */ 0);
}
} else {
return new ErrorInfo(PlaybackErrorEvent.ERROR_IO_OTHER, /* subErrorCode= */ 0);
}
} else if (isRendererExoPlaybackException
&& (rendererFormatSupport == C.FORMAT_UNSUPPORTED_TYPE
|| rendererFormatSupport == C.FORMAT_UNSUPPORTED_SUBTYPE)) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_DECODING_FORMAT_UNSUPPORTED, /* subErrorCode= */ 0);
} else if (isRendererExoPlaybackException
&& rendererFormatSupport == C.FORMAT_EXCEEDS_CAPABILITIES) {
return new ErrorInfo(
PlaybackErrorEvent.ERROR_DECODING_FORMAT_EXCEEDS_CAPABILITIES, /* subErrorCode= */ 0);
} else if (isRendererExoPlaybackException
&& rendererFormatSupport == C.FORMAT_UNSUPPORTED_DRM) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_SCHEME_UNSUPPORTED, /* subErrorCode= */ 0);
} else if (cause instanceof MediaCodecRenderer.DecoderInitializationException) {
@Nullable
String diagnosticsInfo =
((MediaCodecRenderer.DecoderInitializationException) cause).diagnosticInfo;
int subErrorCode = Util.getErrorCodeFromPlatformDiagnosticsInfo(diagnosticsInfo);
return new ErrorInfo(PlaybackErrorEvent.ERROR_DECODER_INIT_FAILED, subErrorCode);
} else if (cause instanceof MediaCodecDecoderException) {
int subErrorCode = ((MediaCodecDecoderException) cause).errorCode;
return new ErrorInfo(PlaybackErrorEvent.ERROR_DECODING_FAILED, subErrorCode);
} else if (cause instanceof OutOfMemoryError) {
return new ErrorInfo(PlaybackErrorEvent.ERROR_DECODING_FAILED, /* subErrorCode= */ 0);
} else if (cause instanceof AudioSink.InitializationException) {
int subErrorCode = ((AudioSink.InitializationException) cause).audioTrackState;
return new ErrorInfo(PlaybackErrorEvent.ERROR_AUDIO_TRACK_INIT_FAILED, subErrorCode);
} else if (cause instanceof AudioSink.WriteException) {
int subErrorCode = ((AudioSink.WriteException) cause).errorCode;
return new ErrorInfo(PlaybackErrorEvent.ERROR_AUDIO_TRACK_WRITE_FAILED, subErrorCode);
} else if (cause instanceof MediaCodec.CryptoException) {
int subErrorCode = ((MediaCodec.CryptoException) cause).getErrorCode();
int errorCode = getDrmErrorCode(subErrorCode);
return new ErrorInfo(errorCode, subErrorCode);
} else {
return new ErrorInfo(PlaybackErrorEvent.ERROR_PLAYER_OTHER, /* subErrorCode= */ 0);
}
}
@Nullable
private static DrmInitData getDrmInitData(ImmutableList<Tracks.Group> trackGroups) {
for (Tracks.Group trackGroup : trackGroups) {
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
if (trackGroup.isTrackSelected(trackIndex)) {
@Nullable DrmInitData drmInitData = trackGroup.getTrackFormat(trackIndex).drmInitData;
if (drmInitData != null) {
return drmInitData;
}
}
}
}
return null;
}
private static int getDrmType(DrmInitData drmInitData) {
for (int i = 0; i < drmInitData.schemeDataCount; i++) {
UUID uuid = drmInitData.get(i).uuid;
if (uuid.equals(C.WIDEVINE_UUID)) {
// TODO(b/77625596): Forward MediaDrm metrics to distinguish between L1 and L3 and to set
// the drm session id.
return PlaybackMetrics.DRM_TYPE_WIDEVINE_L1;
}
if (uuid.equals(C.PLAYREADY_UUID)) {
return PlaybackMetrics.DRM_TYPE_PLAY_READY;
}
if (uuid.equals(C.CLEARKEY_UUID)) {
return PlaybackMetrics.DRM_TYPE_CLEARKEY;
}
}
return PlaybackMetrics.DRM_TYPE_OTHER;
}
@SuppressLint("SwitchIntDef") // Only DRM error codes are relevant here.
private static int getDrmErrorCode(int mediaDrmErrorCode) {
switch (Util.getErrorCodeForMediaDrmErrorCode(mediaDrmErrorCode)) {
case PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED:
return PlaybackErrorEvent.ERROR_DRM_PROVISIONING_FAILED;
case PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED:
return PlaybackErrorEvent.ERROR_DRM_LICENSE_ACQUISITION_FAILED;
case PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION:
return PlaybackErrorEvent.ERROR_DRM_DISALLOWED_OPERATION;
case PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR:
return PlaybackErrorEvent.ERROR_DRM_CONTENT_ERROR;
case PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR:
default:
return PlaybackErrorEvent.ERROR_DRM_SYSTEM_ERROR;
}
}
private static final class ErrorInfo {
public final int errorCode;
public final int subErrorCode;
public ErrorInfo(int errorCode, int subErrorCode) {
this.errorCode = errorCode;
this.subErrorCode = subErrorCode;
}
}
private static final class PendingFormatUpdate {
public final Format format;
public final @C.SelectionReason int selectionReason;
public final String sessionId;
public PendingFormatUpdate(
Format format, @C.SelectionReason int selectionReason, String sessionId) {
this.format = format;
this.selectionReason = selectionReason;
this.sessionId = sessionId;
}
}
}