Move MediaMetricsListener creation and reporting off main thread

The creation can be moved to the playback thread (to guarantee it
happens in sync other initialization after playback start) and the
potentially blocking calls to the reporting methods can be moved
to the generic shared BackgroundExecutor (it can't use the playback
thread because it no longer exists when the session is ended after
the player is released).

PiperOrigin-RevId: 726872818
(cherry picked from commit d386e002d2b34817178d088f277ced3bf3943ef2)
This commit is contained in:
tonihei 2025-02-14 04:30:47 -08:00
parent cd6e61d856
commit 841e27ae5c
4 changed files with 64 additions and 67 deletions

View File

@ -49,7 +49,6 @@ import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo; import android.media.AudioDeviceInfo;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.media.metrics.LogSessionId;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Pair; import android.util.Pair;
@ -354,14 +353,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
playbackInfoUpdateHandler.post(() -> handlePlaybackInfo(playbackInfoUpdate)); playbackInfoUpdateHandler.post(() -> handlePlaybackInfo(playbackInfoUpdate));
playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult); playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult);
analyticsCollector.setPlayer(this.wrappingPlayer, applicationLooper); analyticsCollector.setPlayer(this.wrappingPlayer, applicationLooper);
PlayerId playerId = PlayerId playerId = new PlayerId(builder.playerName);
Util.SDK_INT < 31
? new PlayerId(builder.playerName)
: Api31.registerMediaMetricsListener(
applicationContext,
/* player= */ this,
builder.usePlatformDiagnostics,
builder.playerName);
internalPlayer = internalPlayer =
new ExoPlayerImplInternal( new ExoPlayerImplInternal(
renderers, renderers,
@ -401,6 +393,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (builder.foregroundModeTimeoutMs > 0) { if (builder.foregroundModeTimeoutMs > 0) {
internalPlayer.experimentalSetForegroundModeTimeoutMs(builder.foregroundModeTimeoutMs); internalPlayer.experimentalSetForegroundModeTimeoutMs(builder.foregroundModeTimeoutMs);
} }
if (Util.SDK_INT >= 31) {
Api31.registerMediaMetricsListener(
applicationContext, /* player= */ this, builder.usePlatformDiagnostics, playerId);
}
audioSessionIdState = audioSessionIdState =
new BackgroundThreadStateHandler<>( new BackgroundThreadStateHandler<>(
@ -3383,17 +3379,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
private static final class Api31 { private static final class Api31 {
private Api31() {} private Api31() {}
public static PlayerId registerMediaMetricsListener( public static void registerMediaMetricsListener(
Context context, ExoPlayerImpl player, boolean usePlatformDiagnostics, String playerName) { Context context, ExoPlayerImpl player, boolean usePlatformDiagnostics, PlayerId playerId) {
HandlerWrapper playbackThreadHandler =
player.getClock().createHandler(player.getPlaybackLooper(), /* callback= */ null);
playbackThreadHandler.post(
() -> {
@Nullable MediaMetricsListener listener = MediaMetricsListener.create(context); @Nullable MediaMetricsListener listener = MediaMetricsListener.create(context);
if (listener == null) { if (listener == null) {
Log.w(TAG, "MediaMetricsService unavailable."); Log.w(TAG, "MediaMetricsService unavailable.");
return new PlayerId(LogSessionId.LOG_SESSION_ID_NONE, playerName); return;
} }
if (usePlatformDiagnostics) { if (usePlatformDiagnostics) {
player.addAnalyticsListener(listener); player.addAnalyticsListener(listener);
} }
return new PlayerId(listener.getLogSessionId(), playerName); playerId.setLogSessionId(listener.getLogSessionId());
});
} }
} }

View File

@ -51,6 +51,7 @@ import androidx.media3.common.Player;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.Tracks; import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize; import androidx.media3.common.VideoSize;
import androidx.media3.common.util.BackgroundExecutor;
import androidx.media3.common.util.NetworkTypeObserver; import androidx.media3.common.util.NetworkTypeObserver;
import androidx.media3.common.util.NullableType; import androidx.media3.common.util.NullableType;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
@ -76,6 +77,7 @@ import java.net.SocketTimeoutException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.HashMap; import java.util.HashMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Executor;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@ -108,6 +110,7 @@ public final class MediaMetricsListener
} }
private final Context context; private final Context context;
private final Executor backgroundExecutor;
private final PlaybackSessionManager sessionManager; private final PlaybackSessionManager sessionManager;
private final PlaybackSession playbackSession; private final PlaybackSession playbackSession;
private final long startTimeMs; private final long startTimeMs;
@ -145,6 +148,7 @@ public final class MediaMetricsListener
context = context.getApplicationContext(); context = context.getApplicationContext();
this.context = context; this.context = context;
this.playbackSession = playbackSession; this.playbackSession = playbackSession;
backgroundExecutor = BackgroundExecutor.get();
window = new Timeline.Window(); window = new Timeline.Window();
period = new Timeline.Period(); period = new Timeline.Period();
bandwidthBytes = new HashMap<>(); bandwidthBytes = new HashMap<>();
@ -357,13 +361,14 @@ public final class MediaMetricsListener
ErrorInfo errorInfo = ErrorInfo errorInfo =
getErrorInfo( getErrorInfo(
error, context, /* lastIoErrorForManifest= */ ioErrorType == C.DATA_TYPE_MANIFEST); error, context, /* lastIoErrorForManifest= */ ioErrorType == C.DATA_TYPE_MANIFEST);
playbackSession.reportPlaybackErrorEvent( PlaybackErrorEvent playbackErrorEvent =
new PlaybackErrorEvent.Builder() new PlaybackErrorEvent.Builder()
.setTimeSinceCreatedMillis(realtimeMs - startTimeMs) .setTimeSinceCreatedMillis(realtimeMs - startTimeMs)
.setErrorCode(errorInfo.errorCode) .setErrorCode(errorInfo.errorCode)
.setSubErrorCode(errorInfo.subErrorCode) .setSubErrorCode(errorInfo.subErrorCode)
.setException(error) .setException(error)
.build()); .build();
backgroundExecutor.execute(() -> playbackSession.reportPlaybackErrorEvent(playbackErrorEvent));
reportedEventsForCurrentSession = true; reportedEventsForCurrentSession = true;
pendingPlayerError = null; pendingPlayerError = null;
} }
@ -415,11 +420,12 @@ public final class MediaMetricsListener
int networkType = getNetworkType(context); int networkType = getNetworkType(context);
if (networkType != currentNetworkType) { if (networkType != currentNetworkType) {
currentNetworkType = networkType; currentNetworkType = networkType;
playbackSession.reportNetworkEvent( NetworkEvent networkEvent =
new NetworkEvent.Builder() new NetworkEvent.Builder()
.setNetworkType(networkType) .setNetworkType(networkType)
.setTimeSinceCreatedMillis(realtimeMs - startTimeMs) .setTimeSinceCreatedMillis(realtimeMs - startTimeMs)
.build()); .build();
backgroundExecutor.execute(() -> playbackSession.reportNetworkEvent(networkEvent));
} }
} }
@ -436,11 +442,13 @@ public final class MediaMetricsListener
if (currentPlaybackState != newPlaybackState) { if (currentPlaybackState != newPlaybackState) {
currentPlaybackState = newPlaybackState; currentPlaybackState = newPlaybackState;
reportedEventsForCurrentSession = true; reportedEventsForCurrentSession = true;
playbackSession.reportPlaybackStateEvent( PlaybackStateEvent playbackStateEvent =
new PlaybackStateEvent.Builder() new PlaybackStateEvent.Builder()
.setState(currentPlaybackState) .setState(currentPlaybackState)
.setTimeSinceCreatedMillis(realtimeMs - startTimeMs) .setTimeSinceCreatedMillis(realtimeMs - startTimeMs)
.build()); .build();
backgroundExecutor.execute(
() -> playbackSession.reportPlaybackStateEvent(playbackStateEvent));
} }
} }
@ -570,7 +578,8 @@ public final class MediaMetricsListener
builder.setTrackState(TrackChangeEvent.TRACK_STATE_OFF); builder.setTrackState(TrackChangeEvent.TRACK_STATE_OFF);
} }
reportedEventsForCurrentSession = true; reportedEventsForCurrentSession = true;
playbackSession.reportTrackChangeEvent(builder.build()); TrackChangeEvent trackChangeEvent = builder.build();
backgroundExecutor.execute(() -> playbackSession.reportTrackChangeEvent(trackChangeEvent));
} }
@RequiresNonNull("metricsBuilder") @RequiresNonNull("metricsBuilder")
@ -613,7 +622,8 @@ public final class MediaMetricsListener
networkBytes != null && networkBytes > 0 networkBytes != null && networkBytes > 0
? PlaybackMetrics.STREAM_SOURCE_NETWORK ? PlaybackMetrics.STREAM_SOURCE_NETWORK
: PlaybackMetrics.STREAM_SOURCE_UNKNOWN); : PlaybackMetrics.STREAM_SOURCE_UNKNOWN);
playbackSession.reportPlaybackMetrics(metricsBuilder.build()); PlaybackMetrics playbackMetrics = metricsBuilder.build();
backgroundExecutor.execute(() -> playbackSession.reportPlaybackMetrics(playbackMetrics));
} }
metricsBuilder = null; metricsBuilder = null;
activeSessionId = null; activeSessionId = null;

View File

@ -33,10 +33,7 @@ public final class PlayerId {
/** /**
* A player identifier with unset default values that can be used as a placeholder or for testing. * A player identifier with unset default values that can be used as a placeholder or for testing.
*/ */
public static final PlayerId UNSET = public static final PlayerId UNSET = new PlayerId(/* playerName= */ "");
Util.SDK_INT < 31
? new PlayerId(/* playerName= */ "")
: new PlayerId(LogSessionIdApi31.UNSET, /* playerName= */ "");
/** /**
* A name to identify the player. Use {@link Builder#setName(String)} to set the name, otherwise * A name to identify the player. Use {@link Builder#setName(String)} to set the name, otherwise
@ -52,31 +49,13 @@ public final class PlayerId {
@Nullable private final Object equalityToken; @Nullable private final Object equalityToken;
/** /**
* Creates an instance for API &lt; 31. * Creates an instance.
* *
* @param playerName The name of the player, for informational purpose only. * @param playerName The name of the player, for informational purpose only.
*/ */
public PlayerId(String playerName) { public PlayerId(String playerName) {
checkState(Util.SDK_INT < 31);
this.name = playerName;
this.logSessionIdApi31 = null;
equalityToken = new Object();
}
/**
* Creates an instance for API &ge; 31.
*
* @param logSessionId The {@link LogSessionId} used for this player.
* @param playerName The name of the player, for informational purpose only.
*/
@RequiresApi(31)
public PlayerId(LogSessionId logSessionId, String playerName) {
this(new LogSessionIdApi31(logSessionId), playerName);
}
private PlayerId(LogSessionIdApi31 logSessionIdApi31, String playerName) {
this.logSessionIdApi31 = logSessionIdApi31;
this.name = playerName; this.name = playerName;
this.logSessionIdApi31 = Util.SDK_INT >= 31 ? new LogSessionIdApi31() : null;
equalityToken = new Object(); equalityToken = new Object();
} }
@ -101,19 +80,31 @@ public final class PlayerId {
/** Returns the {@link LogSessionId} for this player instance. */ /** Returns the {@link LogSessionId} for this player instance. */
@RequiresApi(31) @RequiresApi(31)
public LogSessionId getLogSessionId() { public synchronized LogSessionId getLogSessionId() {
return checkNotNull(logSessionIdApi31).logSessionId; return checkNotNull(logSessionIdApi31).logSessionId;
} }
/**
* Set the {@link LogSessionId} for this player instance.
*
* <p>Must not be called if already set.
*/
@RequiresApi(31)
public synchronized void setLogSessionId(LogSessionId logSessionId) {
checkNotNull(logSessionIdApi31).setLogSessionId(logSessionId);
}
@RequiresApi(31) @RequiresApi(31)
private static final class LogSessionIdApi31 { private static final class LogSessionIdApi31 {
public static final LogSessionIdApi31 UNSET = public LogSessionId logSessionId;
new LogSessionIdApi31(LogSessionId.LOG_SESSION_ID_NONE);
public final LogSessionId logSessionId; public LogSessionIdApi31() {
this.logSessionId = LogSessionId.LOG_SESSION_ID_NONE;
}
public LogSessionIdApi31(LogSessionId logSessionId) { public void setLogSessionId(LogSessionId logSessionId) {
checkState(this.logSessionId.equals(LogSessionId.LOG_SESSION_ID_NONE));
this.logSessionId = logSessionId; this.logSessionId = logSessionId;
} }
} }

View File

@ -17,7 +17,6 @@ package androidx.media3.exoplayer;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.media.metrics.LogSessionId;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
@ -59,11 +58,7 @@ public class DefaultLoadControlTest {
public void setUp() throws Exception { public void setUp() throws Exception {
builder = new Builder(); builder = new Builder();
allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
playerId = playerId = new PlayerId(/* playerName= */ "");
Util.SDK_INT < 31
? new PlayerId(/* playerName= */ "")
: new PlayerId(
/* logSessionId= */ LogSessionId.LOG_SESSION_ID_NONE, /* playerName= */ "");
timeline = timeline =
new SinglePeriodTimeline( new SinglePeriodTimeline(
/* durationUs= */ 10_000_000L, /* durationUs= */ 10_000_000L,
@ -130,7 +125,7 @@ public class DefaultLoadControlTest {
/* bufferForPlaybackAfterRebufferMs= */ 0); /* bufferForPlaybackAfterRebufferMs= */ 0);
build(); build();
// A second player uses the load control. // A second player uses the load control.
PlayerId playerId2 = new PlayerId(LogSessionId.LOG_SESSION_ID_NONE, /* playerName= */ ""); PlayerId playerId2 = new PlayerId(/* playerName= */ "");
Timeline timeline2 = new FakeTimeline(); Timeline timeline2 = new FakeTimeline();
MediaSource.MediaPeriodId mediaPeriodId2 = MediaSource.MediaPeriodId mediaPeriodId2 =
new MediaSource.MediaPeriodId( new MediaSource.MediaPeriodId(
@ -731,7 +726,7 @@ public class DefaultLoadControlTest {
@Test @Test
public void onPrepared_updatesTargetBufferBytes_correctDefaultTargetBufferSize() { public void onPrepared_updatesTargetBufferBytes_correctDefaultTargetBufferSize() {
PlayerId playerId2 = new PlayerId(LogSessionId.LOG_SESSION_ID_NONE, /* playerName= */ ""); PlayerId playerId2 = new PlayerId(/* playerName= */ "");
loadControl = builder.setAllocator(allocator).build(); loadControl = builder.setAllocator(allocator).build();
loadControl.onPrepared(playerId); loadControl.onPrepared(playerId);
@ -743,7 +738,7 @@ public class DefaultLoadControlTest {
@Test @Test
public void onTrackSelected_updatesTargetBufferBytes_correctTargetBufferSizeFromTrackType() { public void onTrackSelected_updatesTargetBufferBytes_correctTargetBufferSizeFromTrackType() {
PlayerId playerId2 = new PlayerId(LogSessionId.LOG_SESSION_ID_NONE, /* playerName= */ ""); PlayerId playerId2 = new PlayerId(/* playerName= */ "");
loadControl = builder.setAllocator(allocator).build(); loadControl = builder.setAllocator(allocator).build();
loadControl.onPrepared(playerId); loadControl.onPrepared(playerId);
loadControl.onPrepared(playerId2); loadControl.onPrepared(playerId2);
@ -791,7 +786,7 @@ public class DefaultLoadControlTest {
@Test @Test
public void onRelease_removesLoadingStateOfPlayer() { public void onRelease_removesLoadingStateOfPlayer() {
PlayerId playerId2 = new PlayerId(LogSessionId.LOG_SESSION_ID_NONE, /* playerName= */ ""); PlayerId playerId2 = new PlayerId(/* playerName= */ "");
loadControl = builder.setAllocator(allocator).build(); loadControl = builder.setAllocator(allocator).build();
loadControl.onPrepared(playerId); loadControl.onPrepared(playerId);
loadControl.onPrepared(playerId2); loadControl.onPrepared(playerId2);