From a9e6bc60cb2a232ffbefc2557b8758c8145c9f83 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 20 Jan 2022 16:36:54 +0000 Subject: [PATCH] End finished sessions on timeline updates. We currently only end sessions on Timeline updates if the associated media is no longer in the playlist. But we should also end all sessions that are finished as a result of the timeline update (similar to how this is done for discontinuities). This issue was introduced by https://github.com/google/ExoPlayer/commit/394ab7bcfd53bc8ffb8d2e37a5aeb71fef04d13c PiperOrigin-RevId: 423075855 --- .../DefaultPlaybackSessionManager.java | 3 +- .../DefaultPlaybackSessionManagerTest.java | 122 ++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultPlaybackSessionManager.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultPlaybackSessionManager.java index 865c5946cb..24fc544c32 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultPlaybackSessionManager.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultPlaybackSessionManager.java @@ -179,7 +179,8 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag Iterator iterator = sessions.values().iterator(); while (iterator.hasNext()) { SessionDescriptor session = iterator.next(); - if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline)) { + if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline) + || session.isFinishedAtEventTime(eventTime)) { iterator.remove(); if (session.isCreated) { if (session.sessionId.equals(currentSessionId)) { diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultPlaybackSessionManagerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultPlaybackSessionManagerTest.java index 848976017d..20ba83eceb 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultPlaybackSessionManagerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultPlaybackSessionManagerTest.java @@ -743,6 +743,128 @@ public final class DefaultPlaybackSessionManagerTest { verify(mockListener, never()).onSessionFinished(any(), anyString(), anyBoolean()); } + @Test + public void timelineUpdate_toNewMediaWithWindowIndexOnly_finishesOtherSessions() { + Timeline firstTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1000), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2000), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 3000)); + EventTime eventTimeFirstTimelineWithPeriodId = + createEventTime( + firstTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + firstTimeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); + EventTime eventTimeFirstTimelineWindowOnly1 = + createEventTime(firstTimeline, /* windowIndex= */ 1, /* mediaPeriodId= */ null); + EventTime eventTimeFirstTimelineWindowOnly2 = + createEventTime(firstTimeline, /* windowIndex= */ 2, /* mediaPeriodId= */ null); + Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2000), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 4000)); + EventTime eventTimeSecondTimeline = + createEventTime(secondTimeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + sessionManager.updateSessionsWithTimelineChange(eventTimeFirstTimelineWithPeriodId); + sessionManager.updateSessions(eventTimeFirstTimelineWindowOnly1); + sessionManager.updateSessions(eventTimeFirstTimelineWindowOnly2); + + sessionManager.updateSessionsWithTimelineChange(eventTimeSecondTimeline); + + InOrder inOrder = inOrder(mockListener); + ArgumentCaptor firstId = ArgumentCaptor.forClass(String.class); + inOrder + .verify(mockListener) + .onSessionCreated(eq(eventTimeFirstTimelineWithPeriodId), firstId.capture()); + inOrder + .verify(mockListener) + .onSessionActive(eventTimeFirstTimelineWithPeriodId, firstId.getValue()); + ArgumentCaptor secondId = ArgumentCaptor.forClass(String.class); + inOrder + .verify(mockListener) + .onSessionCreated(eq(eventTimeFirstTimelineWindowOnly1), secondId.capture()); + ArgumentCaptor thirdId = ArgumentCaptor.forClass(String.class); + inOrder + .verify(mockListener) + .onSessionCreated(eq(eventTimeFirstTimelineWindowOnly2), thirdId.capture()); + inOrder + .verify(mockListener) + .onSessionFinished( + eventTimeSecondTimeline, + firstId.getValue(), + /* automaticTransitionToNextPlayback= */ false); + inOrder + .verify(mockListener) + .onSessionFinished( + eventTimeSecondTimeline, + thirdId.getValue(), + /* automaticTransitionToNextPlayback= */ false); + inOrder.verify(mockListener).onSessionActive(eventTimeSecondTimeline, secondId.getValue()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void timelineUpdate_toNewMediaWithMediaPeriodId_finishesOtherSessions() { + Timeline firstTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1000), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2000), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 3000)); + EventTime eventTimeFirstTimeline1 = + createEventTime( + firstTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + firstTimeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); + EventTime eventTimeFirstTimeline2 = + createEventTime( + firstTimeline, + /* windowIndex= */ 1, + new MediaPeriodId( + firstTimeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1)); + EventTime eventTimeFirstTimeline3 = + createEventTime( + firstTimeline, + /* windowIndex= */ 2, + new MediaPeriodId( + firstTimeline.getUidOfPeriod(/* periodIndex= */ 2), /* windowSequenceNumber= */ 2)); + Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2000), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1000), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 3000)); + EventTime eventTimeSecondTimeline = + createEventTime( + secondTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + secondTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* windowSequenceNumber= */ 1)); + sessionManager.updateSessionsWithTimelineChange(eventTimeFirstTimeline1); + sessionManager.updateSessions(eventTimeFirstTimeline2); + sessionManager.updateSessions(eventTimeFirstTimeline3); + + sessionManager.updateSessionsWithTimelineChange(eventTimeSecondTimeline); + + InOrder inOrder = inOrder(mockListener); + ArgumentCaptor firstId = ArgumentCaptor.forClass(String.class); + inOrder.verify(mockListener).onSessionCreated(eq(eventTimeFirstTimeline1), firstId.capture()); + inOrder.verify(mockListener).onSessionActive(eventTimeFirstTimeline1, firstId.getValue()); + ArgumentCaptor secondId = ArgumentCaptor.forClass(String.class); + inOrder.verify(mockListener).onSessionCreated(eq(eventTimeFirstTimeline2), secondId.capture()); + ArgumentCaptor thirdId = ArgumentCaptor.forClass(String.class); + inOrder.verify(mockListener).onSessionCreated(eq(eventTimeFirstTimeline3), thirdId.capture()); + inOrder + .verify(mockListener) + .onSessionFinished( + eventTimeSecondTimeline, + firstId.getValue(), + /* automaticTransitionToNextPlayback= */ false); + inOrder.verify(mockListener).onSessionActive(eventTimeSecondTimeline, secondId.getValue()); + inOrder.verifyNoMoreInteractions(); + } + @Test public void positionDiscontinuity_withinWindow_doesNotFinishSession() { Timeline timeline =