diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/StatsDataSource.java b/libraries/datasource/src/main/java/androidx/media3/datasource/StatsDataSource.java index b75c8e7de0..e30f2caeaf 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/StatsDataSource.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/StatsDataSource.java @@ -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; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DrmUtil.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DrmUtil.java index 2d3a9f9c75..630f72b82c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DrmUtil.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DrmUtil.java @@ -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); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java index 5c525f9798..4cade3e65a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java @@ -602,14 +602,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, int retryCount) { StatsDataSource dataSource = loadable.dataSource; LoadEventInfo loadEventInfo = - new LoadEventInfo( - loadable.loadTaskId, - loadable.dataSpec, - dataSource.getLastOpenedUri(), - dataSource.getLastResponseHeaders(), - elapsedRealtimeMs, - loadDurationMs, - dataSource.getBytesRead()); + retryCount == 0 + ? new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs) + : new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + dataSource.getLastOpenedUri(), + dataSource.getLastResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + dataSource.getBytesRead()); mediaSourceEventDispatcher.loadStarted( loadEventInfo, C.DATA_TYPE_MEDIA, diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SingleSampleMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SingleSampleMediaPeriod.java index 651e296078..1098a35a3e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SingleSampleMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SingleSampleMediaPeriod.java @@ -202,14 +202,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, int retryCount) { StatsDataSource dataSource = loadable.dataSource; LoadEventInfo loadEventInfo = - new LoadEventInfo( - loadable.loadTaskId, - loadable.dataSpec, - dataSource.getLastOpenedUri(), - dataSource.getLastResponseHeaders(), - elapsedRealtimeMs, - loadDurationMs, - dataSource.getBytesRead()); + retryCount == 0 + ? new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs) + : new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + dataSource.getLastOpenedUri(), + dataSource.getLastResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + dataSource.getBytesRead()); eventDispatcher.loadStarted( loadEventInfo, C.DATA_TYPE_MEDIA, diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java index 006f63911b..1b83ab70d2 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java @@ -432,15 +432,19 @@ public class ChunkSampleStream @Override public void onLoadStarted( Chunk 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()); mediaSourceEventDispatcher.loadStarted( - new LoadEventInfo( - loadable.loadTaskId, - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - elapsedRealtimeMs, - loadDurationMs, - loadable.dataSource.getBytesRead()), + loadEventInfo, loadable.type, primaryTrackType, loadable.trackFormat, diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/Loader.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/Loader.java index 308619abea..1456afd143 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/Loader.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/Loader.java @@ -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. + *

This is called for the first time with {@code retryCount == 0} just before 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. diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/AnalyticsListenerPlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/AnalyticsListenerPlaybackTest.java new file mode 100644 index 0000000000..accb0d255b --- /dev/null +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/AnalyticsListenerPlaybackTest.java @@ -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 loadStartedEventInfoCaptor = + ArgumentCaptor.forClass(LoadEventInfo.class); + verify(mockAnalyticsListener, atLeastOnce()) + .onLoadStarted(any(), loadStartedEventInfoCaptor.capture(), any(), anyInt()); + List loadStartedUris = + Lists.transform(loadStartedEventInfoCaptor.getAllValues(), i -> i.uri); + List 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 loadCompletedEventInfoCaptor = + ArgumentCaptor.forClass(LoadEventInfo.class); + verify(mockAnalyticsListener, atLeastOnce()) + .onLoadCompleted(any(), loadCompletedEventInfoCaptor.capture(), any()); + List loadCompletedUris = + Lists.transform(loadCompletedEventInfoCaptor.getAllValues(), i -> i.uri); + List loadCompletedDataSpecUris = + Lists.transform(loadCompletedEventInfoCaptor.getAllValues(), i -> i.dataSpec.uri); + // Every started load should be completed. + assertThat(loadCompletedUris).containsExactlyElementsIn(loadStartedUris); + assertThat(loadCompletedDataSpecUris).containsExactlyElementsIn(loadStartedUris); + } +} diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java index 3bbbf9929c..0ef7df98db 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java @@ -634,17 +634,18 @@ public final class DashMediaSource extends BaseMediaSource { long elapsedRealtimeMs, long loadDurationMs, int retryCount) { - manifestEventDispatcher.loadStarted( - new LoadEventInfo( - loadable.loadTaskId, - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()), - loadable.type, - 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()); + manifestEventDispatcher.loadStarted(loadEventInfo, loadable.type, retryCount); } /* package */ void onManifestLoadCompleted( diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java index 30e88af886..2ff2710072 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java @@ -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 loadStartedEventInfoCaptor = + ArgumentCaptor.forClass(LoadEventInfo.class); + verify(mockAnalyticsListener, atLeastOnce()) + .onLoadStarted(any(), loadStartedEventInfoCaptor.capture(), any(), anyInt()); + List loadStartedUris = + Lists.transform(loadStartedEventInfoCaptor.getAllValues(), i -> i.uri); + List 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 loadCompletedEventInfoCaptor = + ArgumentCaptor.forClass(LoadEventInfo.class); + verify(mockAnalyticsListener, atLeastOnce()) + .onLoadCompleted(any(), loadCompletedEventInfoCaptor.capture(), any()); + List loadCompletedUris = + Lists.transform(loadCompletedEventInfoCaptor.getAllValues(), i -> i.uri); + List 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; diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java index 60b0333983..fbb974a73b 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java @@ -853,15 +853,19 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override public void onLoadStarted( Chunk 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()); mediaSourceEventDispatcher.loadStarted( - new LoadEventInfo( - loadable.loadTaskId, - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()), + loadEventInfo, loadable.type, trackType, loadable.trackFormat, diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java index a242d81b8b..505841af42 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java @@ -252,17 +252,18 @@ public final class DefaultHlsPlaylistTracker long elapsedRealtimeMs, long loadDurationMs, int retryCount) { - eventDispatcher.loadStarted( - new LoadEventInfo( - loadable.loadTaskId, - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()), - loadable.type, - 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 @@ -613,6 +614,26 @@ public final class DefaultHlsPlaylistTracker // Loader.Callback implementation. + @Override + public void onLoadStarted( + ParsingLoadable 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 loadable, long elapsedRealtimeMs, long loadDurationMs) { diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/e2etest/HlsPlaybackTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/e2etest/HlsPlaybackTest.java index cc4cb8b16b..0d6391f871 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/e2etest/HlsPlaybackTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/e2etest/HlsPlaybackTest.java @@ -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 loadStartedEventInfoCaptor = + ArgumentCaptor.forClass(LoadEventInfo.class); + verify(mockAnalyticsListener, atLeastOnce()) + .onLoadStarted(any(), loadStartedEventInfoCaptor.capture(), any(), anyInt()); + List loadStartedUris = + Lists.transform(loadStartedEventInfoCaptor.getAllValues(), i -> i.uri); + List 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 loadCompletedEventInfoCaptor = + ArgumentCaptor.forClass(LoadEventInfo.class); + verify(mockAnalyticsListener, atLeastOnce()) + .onLoadCompleted(any(), loadCompletedEventInfoCaptor.capture(), any()); + List loadCompletedUris = + Lists.transform(loadCompletedEventInfoCaptor.getAllValues(), i -> i.uri); + List 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; diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java index 62a581c7d8..4971ca9a94 100644 --- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java +++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java @@ -503,17 +503,18 @@ public final class SsMediaSource extends BaseMediaSource long elapsedRealtimeMs, long loadDurationMs, int retryCount) { - manifestEventDispatcher.loadStarted( - new LoadEventInfo( - loadable.loadTaskId, - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()), - loadable.type, - 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()); + manifestEventDispatcher.loadStarted(loadEventInfo, loadable.type, retryCount); } @Override