Fix empty LoadEventInfo.uri passed to onLoadStarted

This affects both `AnalyticsListener` and `MediaSourceEventListener`

This was introduced by d051b4b993

Also fix a missing 'load started' event for HLS media playlists (this
was also introduced by d051b4b993).

PiperOrigin-RevId: 691580183
This commit is contained in:
ibaker 2024-10-30 15:49:44 -07:00 committed by Copybara-Service
parent 6cbf77b3f0
commit b5db8a6cbe
13 changed files with 312 additions and 70 deletions

View File

@ -61,7 +61,8 @@ public final class StatsDataSource implements DataSource {
/**
* Returns the {@link Uri} associated with the last {@link #open(DataSpec)} call. If redirection
* occurred, this is the redirected uri.
* occurred, this is the redirected uri. Returns {@link Uri#EMPTY} if {@link #open(DataSpec)} has
* never been called.
*/
public Uri getLastOpenedUri() {
return lastOpenedUri;

View File

@ -30,7 +30,6 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
@ -189,7 +188,7 @@ public final class DrmUtil {
} catch (Exception e) {
throw new MediaDrmCallbackException(
originalDataSpec,
Assertions.checkNotNull(statsDataSource.getLastOpenedUri()),
statsDataSource.getLastOpenedUri(),
statsDataSource.getResponseHeaders(),
statsDataSource.getBytesRead(),
/* cause= */ e);

View File

@ -602,7 +602,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, int retryCount) {
StatsDataSource dataSource = loadable.dataSource;
LoadEventInfo loadEventInfo =
new LoadEventInfo(
retryCount == 0
? new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs)
: new LoadEventInfo(
loadable.loadTaskId,
loadable.dataSpec,
dataSource.getLastOpenedUri(),

View File

@ -202,7 +202,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, int retryCount) {
StatsDataSource dataSource = loadable.dataSource;
LoadEventInfo loadEventInfo =
new LoadEventInfo(
retryCount == 0
? new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs)
: new LoadEventInfo(
loadable.loadTaskId,
loadable.dataSpec,
dataSource.getLastOpenedUri(),

View File

@ -432,15 +432,19 @@ public class ChunkSampleStream<T extends ChunkSource>
@Override
public void onLoadStarted(
Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, int retryCount) {
mediaSourceEventDispatcher.loadStarted(
new LoadEventInfo(
LoadEventInfo loadEventInfo =
retryCount == 0
? new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs)
: new LoadEventInfo(
loadable.loadTaskId,
loadable.dataSpec,
loadable.getUri(),
loadable.getResponseHeaders(),
elapsedRealtimeMs,
loadDurationMs,
loadable.dataSource.getBytesRead()),
loadable.bytesLoaded());
mediaSourceEventDispatcher.loadStarted(
loadEventInfo,
loadable.type,
primaryTrackType,
loadable.trackFormat,

View File

@ -93,7 +93,10 @@ public final class Loader implements LoaderErrorThrower {
/**
* Called when a load has started for the first time or through a retry.
*
* @param loadable The loadable whose load has completed.
* <p>This is called for the first time with {@code retryCount == 0} just <b>before</b> the load
* is started.
*
* @param loadable The loadable whose load has started.
* @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load attempts to start.
* @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading}
* was called.

View File

@ -0,0 +1,95 @@
/*
* Copyright 2024 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.e2etest;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.net.Uri;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.test.utils.FakeClock;
import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
/** End-to-end tests of events reported to {@link AnalyticsListener}. */
@RunWith(AndroidJUnit4.class)
public class AnalyticsListenerPlaybackTest {
@Rule
public ShadowMediaCodecConfig mediaCodecConfig =
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
@Test
public void loadEventsReportedAsExpected() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
ExoPlayer player =
new ExoPlayer.Builder(applicationContext)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
AnalyticsListener mockAnalyticsListener = mock(AnalyticsListener.class);
player.addAnalyticsListener(mockAnalyticsListener);
Uri mediaUri = Uri.parse("asset:///media/mp4/sample.mp4");
MediaItem mediaItem = new MediaItem.Builder().setUri(mediaUri).build();
player.setMediaItem(mediaItem);
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
ArgumentCaptor<LoadEventInfo> loadStartedEventInfoCaptor =
ArgumentCaptor.forClass(LoadEventInfo.class);
verify(mockAnalyticsListener, atLeastOnce())
.onLoadStarted(any(), loadStartedEventInfoCaptor.capture(), any(), anyInt());
List<Uri> loadStartedUris =
Lists.transform(loadStartedEventInfoCaptor.getAllValues(), i -> i.uri);
List<Uri> loadStartedDataSpecUris =
Lists.transform(loadStartedEventInfoCaptor.getAllValues(), i -> i.dataSpec.uri);
// Remove duplicates in case the load was split into multiple reads.
assertThat(ImmutableSet.copyOf(loadStartedUris)).containsExactly(mediaUri);
// The two sources of URI should match (because there's no redirection).
assertThat(loadStartedDataSpecUris).containsExactlyElementsIn(loadStartedUris).inOrder();
ArgumentCaptor<LoadEventInfo> loadCompletedEventInfoCaptor =
ArgumentCaptor.forClass(LoadEventInfo.class);
verify(mockAnalyticsListener, atLeastOnce())
.onLoadCompleted(any(), loadCompletedEventInfoCaptor.capture(), any());
List<Uri> loadCompletedUris =
Lists.transform(loadCompletedEventInfoCaptor.getAllValues(), i -> i.uri);
List<Uri> loadCompletedDataSpecUris =
Lists.transform(loadCompletedEventInfoCaptor.getAllValues(), i -> i.dataSpec.uri);
// Every started load should be completed.
assertThat(loadCompletedUris).containsExactlyElementsIn(loadStartedUris);
assertThat(loadCompletedDataSpecUris).containsExactlyElementsIn(loadStartedUris);
}
}

View File

@ -634,17 +634,18 @@ public final class DashMediaSource extends BaseMediaSource {
long elapsedRealtimeMs,
long loadDurationMs,
int retryCount) {
manifestEventDispatcher.loadStarted(
new LoadEventInfo(
LoadEventInfo loadEventInfo =
retryCount == 0
? new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs)
: new LoadEventInfo(
loadable.loadTaskId,
loadable.dataSpec,
loadable.getUri(),
loadable.getResponseHeaders(),
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded()),
loadable.type,
retryCount);
loadable.bytesLoaded());
manifestEventDispatcher.loadStarted(loadEventInfo, loadable.type, retryCount);
}
/* package */ void onManifestLoadCompleted(

View File

@ -18,6 +18,11 @@ package androidx.media3.exoplayer.dash.e2etest;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.run;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.graphics.SurfaceTexture;
@ -53,10 +58,14 @@ import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
/** End-to-end tests using DASH samples. */
@RunWith(AndroidJUnit4.class)
@ -651,7 +660,52 @@ public final class DashPlaybackTest {
applicationContext, playbackOutput, "playbackdumps/dash/multi-period-with-offset.dump");
}
private static class AnalyticsListenerImpl implements AnalyticsListener {
@Test
public void loadEventsReportedAsExpected() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
ExoPlayer player =
new ExoPlayer.Builder(applicationContext)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
AnalyticsListenerImpl analyticsListener = new AnalyticsListenerImpl();
player.addAnalyticsListener(analyticsListener);
AnalyticsListener mockAnalyticsListener = mock(AnalyticsListener.class);
player.addAnalyticsListener(mockAnalyticsListener);
Uri manifestUri = Uri.parse("asset:///media/dash/emsg/sample.mpd");
player.setMediaItem(MediaItem.fromUri(manifestUri));
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
ArgumentCaptor<LoadEventInfo> loadStartedEventInfoCaptor =
ArgumentCaptor.forClass(LoadEventInfo.class);
verify(mockAnalyticsListener, atLeastOnce())
.onLoadStarted(any(), loadStartedEventInfoCaptor.capture(), any(), anyInt());
List<Uri> loadStartedUris =
Lists.transform(loadStartedEventInfoCaptor.getAllValues(), i -> i.uri);
List<Uri> loadStartedDataSpecUris =
Lists.transform(loadStartedEventInfoCaptor.getAllValues(), i -> i.dataSpec.uri);
// Remove duplicates in case the load was split into multiple reads.
assertThat(ImmutableSet.copyOf(loadStartedUris))
.containsExactly(manifestUri, Uri.parse("asset:///media/dash/emsg/sample.audio.mp4"));
// The two sources of URI should match (because there's no redirection).
assertThat(loadStartedDataSpecUris).containsExactlyElementsIn(loadStartedUris).inOrder();
ArgumentCaptor<LoadEventInfo> loadCompletedEventInfoCaptor =
ArgumentCaptor.forClass(LoadEventInfo.class);
verify(mockAnalyticsListener, atLeastOnce())
.onLoadCompleted(any(), loadCompletedEventInfoCaptor.capture(), any());
List<Uri> loadCompletedUris =
Lists.transform(loadCompletedEventInfoCaptor.getAllValues(), i -> i.uri);
List<Uri> loadCompletedDataSpecUris =
Lists.transform(loadCompletedEventInfoCaptor.getAllValues(), i -> i.dataSpec.uri);
// Every started load should be completed.
assertThat(loadCompletedUris).containsExactlyElementsIn(loadStartedUris);
assertThat(loadCompletedDataSpecUris).containsExactlyElementsIn(loadStartedUris);
}
private static final class AnalyticsListenerImpl implements AnalyticsListener {
@Nullable private LoadEventInfo loadErrorEventInfo;
@Nullable private IOException loadError;

View File

@ -853,15 +853,19 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Override
public void onLoadStarted(
Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, int retryCount) {
mediaSourceEventDispatcher.loadStarted(
new LoadEventInfo(
LoadEventInfo loadEventInfo =
retryCount == 0
? new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs)
: new LoadEventInfo(
loadable.loadTaskId,
loadable.dataSpec,
loadable.getUri(),
loadable.getResponseHeaders(),
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded()),
loadable.bytesLoaded());
mediaSourceEventDispatcher.loadStarted(
loadEventInfo,
loadable.type,
trackType,
loadable.trackFormat,

View File

@ -252,17 +252,18 @@ public final class DefaultHlsPlaylistTracker
long elapsedRealtimeMs,
long loadDurationMs,
int retryCount) {
eventDispatcher.loadStarted(
new LoadEventInfo(
LoadEventInfo loadEventInfo =
retryCount == 0
? new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs)
: new LoadEventInfo(
loadable.loadTaskId,
loadable.dataSpec,
loadable.getUri(),
loadable.getResponseHeaders(),
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded()),
loadable.type,
retryCount);
loadable.bytesLoaded());
eventDispatcher.loadStarted(loadEventInfo, loadable.type, retryCount);
}
@Override
@ -613,6 +614,26 @@ public final class DefaultHlsPlaylistTracker
// Loader.Callback implementation.
@Override
public void onLoadStarted(
ParsingLoadable<HlsPlaylist> loadable,
long elapsedRealtimeMs,
long loadDurationMs,
int retryCount) {
LoadEventInfo loadEventInfo =
retryCount == 0
? new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs)
: new LoadEventInfo(
loadable.loadTaskId,
loadable.dataSpec,
loadable.getUri(),
loadable.getResponseHeaders(),
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded());
eventDispatcher.loadStarted(loadEventInfo, loadable.type, retryCount);
}
@Override
public void onLoadCompleted(
ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) {

View File

@ -18,6 +18,11 @@ package androidx.media3.exoplayer.hls.e2etest;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.run;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.graphics.SurfaceTexture;
@ -46,10 +51,14 @@ import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
/** End-to-end tests using HLS samples. */
@RunWith(AndroidJUnit4.class)
@ -391,6 +400,52 @@ public final class HlsPlaybackTest {
"playbackdumps/hls/cmcd-enabled-with-init-segment.dump");
}
@Test
public void loadEventsReportedAsExpected() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
ExoPlayer player =
new ExoPlayer.Builder(applicationContext)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
AnalyticsListener mockAnalyticsListener = mock(AnalyticsListener.class);
player.addAnalyticsListener(mockAnalyticsListener);
Uri manifestUri = Uri.parse("asset:///media/hls/cea608/manifest.m3u8");
player.setMediaItem(MediaItem.fromUri(manifestUri));
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
ArgumentCaptor<LoadEventInfo> loadStartedEventInfoCaptor =
ArgumentCaptor.forClass(LoadEventInfo.class);
verify(mockAnalyticsListener, atLeastOnce())
.onLoadStarted(any(), loadStartedEventInfoCaptor.capture(), any(), anyInt());
List<Uri> loadStartedUris =
Lists.transform(loadStartedEventInfoCaptor.getAllValues(), i -> i.uri);
List<Uri> loadStartedDataSpecUris =
Lists.transform(loadStartedEventInfoCaptor.getAllValues(), i -> i.dataSpec.uri);
// Remove duplicates in case the load was split into multiple reads.
assertThat(ImmutableSet.copyOf(loadStartedUris))
.containsExactly(
manifestUri,
Uri.parse("asset:///media/hls/cea608/sd-hls.m3u8"),
Uri.parse("asset:///media/hls/cea608/sd-hls0000000000.ts"));
// The two sources of URI should match (because there's no redirection).
assertThat(loadStartedDataSpecUris).containsExactlyElementsIn(loadStartedUris).inOrder();
ArgumentCaptor<LoadEventInfo> loadCompletedEventInfoCaptor =
ArgumentCaptor.forClass(LoadEventInfo.class);
verify(mockAnalyticsListener, atLeastOnce())
.onLoadCompleted(any(), loadCompletedEventInfoCaptor.capture(), any());
List<Uri> loadCompletedUris =
Lists.transform(loadCompletedEventInfoCaptor.getAllValues(), i -> i.uri);
List<Uri> loadCompletedDataSpecUris =
Lists.transform(loadCompletedEventInfoCaptor.getAllValues(), i -> i.dataSpec.uri);
// Every started load should be completed.
assertThat(loadCompletedUris).containsExactlyElementsIn(loadStartedUris);
assertThat(loadCompletedDataSpecUris).containsExactlyElementsIn(loadStartedUris);
}
private static class AnalyticsListenerImpl implements AnalyticsListener {
@Nullable private LoadEventInfo loadErrorEventInfo;

View File

@ -503,17 +503,18 @@ public final class SsMediaSource extends BaseMediaSource
long elapsedRealtimeMs,
long loadDurationMs,
int retryCount) {
manifestEventDispatcher.loadStarted(
new LoadEventInfo(
LoadEventInfo loadEventInfo =
retryCount == 0
? new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs)
: new LoadEventInfo(
loadable.loadTaskId,
loadable.dataSpec,
loadable.getUri(),
loadable.getResponseHeaders(),
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded()),
loadable.type,
retryCount);
loadable.bytesLoaded());
manifestEventDispatcher.loadStarted(loadEventInfo, loadable.type, retryCount);
}
@Override