Start early-enabled renderers only after advancing the playing period

Renderers may be enabled for subsequent media items as soon as the current media item's renderer's isEnded() returns true. When a renderer is being enabled and the player is 'playing', that renderer is also started. When playing a mixed playlist of images and content with audio & video, the player may skip some image items because the early-starting of the audio renderer causes a clock update.

A solution is to only start the "early-enabled" renderers at the point of media transition and add a condition on DefaultMediaClock to use the standalone clock when reading-ahead and the renderer clock source is not in a started state.

Issue: androidx/media#1017
PiperOrigin-RevId: 613231227
(cherry picked from commit 638b2a3c86f7362041b09faed7c3c09609e29118)
This commit is contained in:
michaelkatz 2024-03-06 08:47:42 -08:00 committed by SheenaChhabra
parent 3fdd3bdb7a
commit 71bfdd1bce
6 changed files with 761 additions and 7 deletions

View File

@ -7,6 +7,9 @@
is preloaded again.
* Apply the correct corresponding `TrackSelectionResult` to the playing
period in track reselection.
* Start early-enabled renderers only after advancing the playing period
when transitioning between media items
([#1017](https://github.com/androidx/media/issues/1017)).
* Transformer:
* Add workaround for exception thrown due to `MediaMuxer` not supporting
negative presentation timestamps before API 30.

View File

@ -15,6 +15,8 @@
*/
package androidx.media3.exoplayer;
import static androidx.media3.exoplayer.Renderer.STATE_STARTED;
import androidx.annotation.Nullable;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
@ -192,12 +194,14 @@ import androidx.media3.common.util.Clock;
}
private boolean shouldUseStandaloneClock(boolean isReadingAhead) {
// Use the standalone clock if the clock providing renderer is not set or has ended. Also use
// Use the standalone clock if the clock providing renderer is not set or has ended. Use the
// standalone clock if reading ahead and the renderer is not in a started state. Also use
// the standalone clock if the renderer is not ready and we have finished reading the stream or
// are reading ahead to avoid getting stuck if tracks in the current period have uneven
// durations. See: https://github.com/google/ExoPlayer/issues/1874.
return rendererClockSource == null
|| rendererClockSource.isEnded()
|| (isReadingAhead && rendererClockSource.getState() != STATE_STARTED)
|| (!rendererClockSource.isReady()
&& (isReadingAhead || rendererClockSource.hasReadStreamToEnd()));
}

View File

@ -18,6 +18,9 @@ package androidx.media3.exoplayer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.common.util.Util.msToUs;
import static androidx.media3.exoplayer.Renderer.STATE_DISABLED;
import static androidx.media3.exoplayer.Renderer.STATE_ENABLED;
import static androidx.media3.exoplayer.Renderer.STATE_STARTED;
import static androidx.media3.exoplayer.audio.AudioSink.OFFLOAD_MODE_DISABLED;
import static java.lang.Math.max;
import static java.lang.Math.min;
@ -76,6 +79,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
@ -1777,7 +1781,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private void ensureStopped(Renderer renderer) {
if (renderer.getState() == Renderer.STATE_STARTED) {
if (renderer.getState() == STATE_STARTED) {
renderer.stop();
}
}
@ -2319,6 +2323,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
resetPendingPauseAtEndOfPeriod();
updatePlaybackPositions();
if (playbackInfo.playbackState == Player.STATE_READY) {
for (int i = 0; i < renderers.length; i++) {
if (renderers[i].getState() == STATE_ENABLED
&& queue.getPlayingPeriod() != null
&& Objects.equals(
renderers[i].getStream(), queue.getPlayingPeriod().sampleStreams[i])) {
renderers[i].start();
}
}
}
allowRenderersToRenderStartOfStreams();
advancedPlayingPeriod = true;
}
@ -2665,7 +2679,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
return;
}
MediaPeriodHolder periodHolder = queue.getReadingPeriod();
boolean mayRenderStartOfStream = periodHolder == queue.getPlayingPeriod();
boolean arePlayingAndReadingTheSamePeriod = periodHolder == queue.getPlayingPeriod();
TrackSelectorResult trackSelectorResult = periodHolder.getTrackSelectorResult();
RendererConfiguration rendererConfiguration =
trackSelectorResult.rendererConfigurations[rendererIndex];
@ -2684,7 +2698,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
periodHolder.sampleStreams[rendererIndex],
rendererPositionUs,
joining,
mayRenderStartOfStream,
/* mayRenderStartOfStream= */ arePlayingAndReadingTheSamePeriod,
startPositionUs,
periodHolder.getRendererOffset(),
periodHolder.info.id);
@ -2703,8 +2717,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
});
mediaClock.onRendererEnabled(renderer);
// Start the renderer if playing.
if (playing) {
// Start the renderer if playing and the Playing and Reading periods are the same.
if (playing && arePlayingAndReadingTheSamePeriod) {
renderer.start();
}
}
@ -3224,7 +3238,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private static boolean isRendererEnabled(Renderer renderer) {
return renderer.getState() != Renderer.STATE_DISABLED;
return renderer.getState() != STATE_DISABLED;
}
private static final class SeekPosition {

View File

@ -15,6 +15,9 @@
*/
package androidx.media3.exoplayer.e2etest;
import static com.google.common.truth.Truth.assertThat;
import static org.robolectric.annotation.GraphicsMode.Mode.NATIVE;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.net.Uri;
@ -23,6 +26,7 @@ import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.Player;
import androidx.media3.common.util.Clock;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
@ -38,9 +42,11 @@ import com.google.common.collect.ImmutableList;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.GraphicsMode;
/** End-to-end tests for playlists. */
@RunWith(AndroidJUnit4.class)
@GraphicsMode(value = NATIVE)
public final class PlaylistPlaybackTest {
@Rule
@ -141,4 +147,39 @@ public final class PlaylistPlaybackTest {
DumpFileAsserts.assertOutput(
applicationContext, playbackOutput, "playbackdumps/playlists/playlist_with_subtitles.dump");
}
@Test
public void testPlaylist_withImageAndAudioVideoItems_rendersExpectedContent() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
CapturingRenderersFactory renderersFactory = new CapturingRenderersFactory(applicationContext);
Clock clock = new FakeClock(/* isAutoAdvancing= */ true);
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, renderersFactory).setClock(clock).build();
PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory);
long durationMs = 5 * C.MILLIS_PER_SECOND;
player.setMediaItems(
ImmutableList.of(
new MediaItem.Builder()
.setUri("asset:///media/png/media3test.png")
.setImageDurationMs(durationMs)
.build(),
new MediaItem.Builder()
.setUri("asset:///media/png/media3test.png")
.setImageDurationMs(durationMs)
.build(),
MediaItem.fromUri("asset:///media/mp4/sample.mp4")));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long playerStartedMs = clock.elapsedRealtime();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
long playbackDurationMs = clock.elapsedRealtime() - playerStartedMs;
player.release();
// Playback duration should be greater than the sum of the image item durations.
assertThat(playbackDurationMs).isGreaterThan(durationMs * 2);
DumpFileAsserts.assertOutput(
applicationContext, playbackOutput, "playbackdumps/playlists/image_av_playlist.dump");
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1,692 @@
MediaCodecAdapter (exotest.audio.aac):
inputBuffers:
count = 46
input buffer #0:
timeUs = 1000010044000
contents = length 23, hash 47DE9131
input buffer #1:
timeUs = 1000010067219
contents = length 6, hash 31EC5206
input buffer #2:
timeUs = 1000010090439
contents = length 148, hash 894A176B
input buffer #3:
timeUs = 1000010113659
contents = length 189, hash CEF235A1
input buffer #4:
timeUs = 1000010136879
contents = length 205, hash BBF5F7B0
input buffer #5:
timeUs = 1000010160099
contents = length 210, hash F278B193
input buffer #6:
timeUs = 1000010183319
contents = length 210, hash 82DA1589
input buffer #7:
timeUs = 1000010206539
contents = length 207, hash 5BE231DF
input buffer #8:
timeUs = 1000010229759
contents = length 225, hash 18819EE1
input buffer #9:
timeUs = 1000010252979
contents = length 215, hash CA7FA67B
input buffer #10:
timeUs = 1000010276199
contents = length 211, hash 581A1C18
input buffer #11:
timeUs = 1000010299419
contents = length 216, hash ADB88187
input buffer #12:
timeUs = 1000010322639
contents = length 229, hash 2E8BA4DC
input buffer #13:
timeUs = 1000010345859
contents = length 232, hash 22F0C510
input buffer #14:
timeUs = 1000010369079
contents = length 235, hash 867AD0DC
input buffer #15:
timeUs = 1000010392299
contents = length 231, hash 84E823A8
input buffer #16:
timeUs = 1000010415519
contents = length 226, hash 1BEF3A95
input buffer #17:
timeUs = 1000010438739
contents = length 216, hash EAA345AE
input buffer #18:
timeUs = 1000010461959
contents = length 229, hash 6957411F
input buffer #19:
timeUs = 1000010485179
contents = length 219, hash 41275022
input buffer #20:
timeUs = 1000010508399
contents = length 241, hash 6495DF96
input buffer #21:
timeUs = 1000010531619
contents = length 228, hash 63D95906
input buffer #22:
timeUs = 1000010554839
contents = length 238, hash 34F676F9
input buffer #23:
timeUs = 1000010578058
contents = length 234, hash E5CBC045
input buffer #24:
timeUs = 1000010601278
contents = length 231, hash 5FC43661
input buffer #25:
timeUs = 1000010624498
contents = length 217, hash 682708ED
input buffer #26:
timeUs = 1000010647718
contents = length 239, hash D43780FC
input buffer #27:
timeUs = 1000010670938
contents = length 243, hash C5E17980
input buffer #28:
timeUs = 1000010694158
contents = length 231, hash AC5837BA
input buffer #29:
timeUs = 1000010717378
contents = length 230, hash 169EE895
input buffer #30:
timeUs = 1000010740598
contents = length 238, hash C48FF3F1
input buffer #31:
timeUs = 1000010763818
contents = length 225, hash 531E4599
input buffer #32:
timeUs = 1000010787038
contents = length 232, hash CB3C6B8D
input buffer #33:
timeUs = 1000010810258
contents = length 243, hash F8C94C7
input buffer #34:
timeUs = 1000010833478
contents = length 232, hash A646A7D0
input buffer #35:
timeUs = 1000010856698
contents = length 237, hash E8B787A5
input buffer #36:
timeUs = 1000010879918
contents = length 228, hash 3FA7A29F
input buffer #37:
timeUs = 1000010903138
contents = length 235, hash B9B33B0A
input buffer #38:
timeUs = 1000010926358
contents = length 264, hash 71A4869E
input buffer #39:
timeUs = 1000010949578
contents = length 257, hash D049B54C
input buffer #40:
timeUs = 1000010972798
contents = length 227, hash 66757231
input buffer #41:
timeUs = 1000010996018
contents = length 227, hash BD374F1B
input buffer #42:
timeUs = 1000011019238
contents = length 235, hash 999477F6
input buffer #43:
timeUs = 1000011042458
contents = length 229, hash FFF98DF0
input buffer #44:
timeUs = 1000011065678
contents = length 6, hash 31B22286
input buffer #45:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 45
output buffer #0:
timeUs = 1000010044000
size = 0
rendered = false
output buffer #1:
timeUs = 1000010067219
size = 0
rendered = false
output buffer #2:
timeUs = 1000010090439
size = 0
rendered = false
output buffer #3:
timeUs = 1000010113659
size = 0
rendered = false
output buffer #4:
timeUs = 1000010136879
size = 0
rendered = false
output buffer #5:
timeUs = 1000010160099
size = 0
rendered = false
output buffer #6:
timeUs = 1000010183319
size = 0
rendered = false
output buffer #7:
timeUs = 1000010206539
size = 0
rendered = false
output buffer #8:
timeUs = 1000010229759
size = 0
rendered = false
output buffer #9:
timeUs = 1000010252979
size = 0
rendered = false
output buffer #10:
timeUs = 1000010276199
size = 0
rendered = false
output buffer #11:
timeUs = 1000010299419
size = 0
rendered = false
output buffer #12:
timeUs = 1000010322639
size = 0
rendered = false
output buffer #13:
timeUs = 1000010345859
size = 0
rendered = false
output buffer #14:
timeUs = 1000010369079
size = 0
rendered = false
output buffer #15:
timeUs = 1000010392299
size = 0
rendered = false
output buffer #16:
timeUs = 1000010415519
size = 0
rendered = false
output buffer #17:
timeUs = 1000010438739
size = 0
rendered = false
output buffer #18:
timeUs = 1000010461959
size = 0
rendered = false
output buffer #19:
timeUs = 1000010485179
size = 0
rendered = false
output buffer #20:
timeUs = 1000010508399
size = 0
rendered = false
output buffer #21:
timeUs = 1000010531619
size = 0
rendered = false
output buffer #22:
timeUs = 1000010554839
size = 0
rendered = false
output buffer #23:
timeUs = 1000010578058
size = 0
rendered = false
output buffer #24:
timeUs = 1000010601278
size = 0
rendered = false
output buffer #25:
timeUs = 1000010624498
size = 0
rendered = false
output buffer #26:
timeUs = 1000010647718
size = 0
rendered = false
output buffer #27:
timeUs = 1000010670938
size = 0
rendered = false
output buffer #28:
timeUs = 1000010694158
size = 0
rendered = false
output buffer #29:
timeUs = 1000010717378
size = 0
rendered = false
output buffer #30:
timeUs = 1000010740598
size = 0
rendered = false
output buffer #31:
timeUs = 1000010763818
size = 0
rendered = false
output buffer #32:
timeUs = 1000010787038
size = 0
rendered = false
output buffer #33:
timeUs = 1000010810258
size = 0
rendered = false
output buffer #34:
timeUs = 1000010833478
size = 0
rendered = false
output buffer #35:
timeUs = 1000010856698
size = 0
rendered = false
output buffer #36:
timeUs = 1000010879918
size = 0
rendered = false
output buffer #37:
timeUs = 1000010903138
size = 0
rendered = false
output buffer #38:
timeUs = 1000010926358
size = 0
rendered = false
output buffer #39:
timeUs = 1000010949578
size = 0
rendered = false
output buffer #40:
timeUs = 1000010972798
size = 0
rendered = false
output buffer #41:
timeUs = 1000010996018
size = 0
rendered = false
output buffer #42:
timeUs = 1000011019238
size = 0
rendered = false
output buffer #43:
timeUs = 1000011042458
size = 0
rendered = false
output buffer #44:
timeUs = 1000011065678
size = 0
rendered = false
MediaCodecAdapter (exotest.video.avc):
inputBuffers:
count = 31
input buffer #0:
timeUs = 1000010000000
contents = length 36692, hash D216076E
input buffer #1:
timeUs = 1000010066733
contents = length 5312, hash D45D3CA0
input buffer #2:
timeUs = 1000010033366
contents = length 599, hash 1BE7812D
input buffer #3:
timeUs = 1000010200200
contents = length 7735, hash 4490F110
input buffer #4:
timeUs = 1000010133466
contents = length 987, hash 560B5036
input buffer #5:
timeUs = 1000010100100
contents = length 673, hash ED7CD8C7
input buffer #6:
timeUs = 1000010166833
contents = length 523, hash 3020DF50
input buffer #7:
timeUs = 1000010333666
contents = length 6061, hash 736C72B2
input buffer #8:
timeUs = 1000010266933
contents = length 992, hash FE132F23
input buffer #9:
timeUs = 1000010233566
contents = length 623, hash 5B2C1816
input buffer #10:
timeUs = 1000010300300
contents = length 421, hash 742E69C1
input buffer #11:
timeUs = 1000010433766
contents = length 4899, hash F72F86A1
input buffer #12:
timeUs = 1000010400400
contents = length 568, hash 519A8E50
input buffer #13:
timeUs = 1000010367033
contents = length 620, hash 3990AA39
input buffer #14:
timeUs = 1000010567233
contents = length 5450, hash F06EC4AA
input buffer #15:
timeUs = 1000010500500
contents = length 1051, hash 92DFA63A
input buffer #16:
timeUs = 1000010467133
contents = length 874, hash 69587FB4
input buffer #17:
timeUs = 1000010533866
contents = length 781, hash 36BE495B
input buffer #18:
timeUs = 1000010700700
contents = length 4725, hash AC0C8CD3
input buffer #19:
timeUs = 1000010633966
contents = length 1022, hash 5D8BFF34
input buffer #20:
timeUs = 1000010600600
contents = length 790, hash 99413A99
input buffer #21:
timeUs = 1000010667333
contents = length 610, hash 5E129290
input buffer #22:
timeUs = 1000010834166
contents = length 2751, hash 769974CB
input buffer #23:
timeUs = 1000010767433
contents = length 745, hash B78A477A
input buffer #24:
timeUs = 1000010734066
contents = length 621, hash CF741E7A
input buffer #25:
timeUs = 1000010800800
contents = length 505, hash 1DB4894E
input buffer #26:
timeUs = 1000010967633
contents = length 1268, hash C15348DC
input buffer #27:
timeUs = 1000010900900
contents = length 880, hash C2DE85D0
input buffer #28:
timeUs = 1000010867533
contents = length 530, hash C98BC6A8
input buffer #29:
timeUs = 1000010934266
contents = length 568, hash 4FE5C8EA
input buffer #30:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 30
output buffer #0:
timeUs = 1000010000000
size = 36692
rendered = false
output buffer #1:
timeUs = 1000010066733
size = 5312
rendered = false
output buffer #2:
timeUs = 1000010033366
size = 599
rendered = false
output buffer #3:
timeUs = 1000010200200
size = 7735
rendered = false
output buffer #4:
timeUs = 1000010133466
size = 987
rendered = false
output buffer #5:
timeUs = 1000010100100
size = 673
rendered = false
output buffer #6:
timeUs = 1000010166833
size = 523
rendered = false
output buffer #7:
timeUs = 1000010333666
size = 6061
rendered = false
output buffer #8:
timeUs = 1000010266933
size = 992
rendered = false
output buffer #9:
timeUs = 1000010233566
size = 623
rendered = false
output buffer #10:
timeUs = 1000010300300
size = 421
rendered = false
output buffer #11:
timeUs = 1000010433766
size = 4899
rendered = false
output buffer #12:
timeUs = 1000010400400
size = 568
rendered = false
output buffer #13:
timeUs = 1000010367033
size = 620
rendered = false
output buffer #14:
timeUs = 1000010567233
size = 5450
rendered = false
output buffer #15:
timeUs = 1000010500500
size = 1051
rendered = false
output buffer #16:
timeUs = 1000010467133
size = 874
rendered = false
output buffer #17:
timeUs = 1000010533866
size = 781
rendered = false
output buffer #18:
timeUs = 1000010700700
size = 4725
rendered = false
output buffer #19:
timeUs = 1000010633966
size = 1022
rendered = false
output buffer #20:
timeUs = 1000010600600
size = 790
rendered = false
output buffer #21:
timeUs = 1000010667333
size = 610
rendered = false
output buffer #22:
timeUs = 1000010834166
size = 2751
rendered = false
output buffer #23:
timeUs = 1000010767433
size = 745
rendered = false
output buffer #24:
timeUs = 1000010734066
size = 621
rendered = false
output buffer #25:
timeUs = 1000010800800
size = 505
rendered = false
output buffer #26:
timeUs = 1000010967633
size = 1268
rendered = false
output buffer #27:
timeUs = 1000010900900
size = 880
rendered = false
output buffer #28:
timeUs = 1000010867533
size = 530
rendered = false
output buffer #29:
timeUs = 1000010934266
size = 568
rendered = false
AudioSink:
buffer count = 45
config:
pcmEncoding = 2
channelCount = 1
sampleRate = 44100
buffer #0:
time = 1000010044000
data = 1
buffer #1:
time = 1000010067219
data = 1
buffer #2:
time = 1000010090439
data = 1
buffer #3:
time = 1000010113659
data = 1
buffer #4:
time = 1000010136879
data = 1
buffer #5:
time = 1000010160099
data = 1
buffer #6:
time = 1000010183319
data = 1
buffer #7:
time = 1000010206539
data = 1
buffer #8:
time = 1000010229759
data = 1
buffer #9:
time = 1000010252979
data = 1
buffer #10:
time = 1000010276199
data = 1
buffer #11:
time = 1000010299419
data = 1
buffer #12:
time = 1000010322639
data = 1
buffer #13:
time = 1000010345859
data = 1
buffer #14:
time = 1000010369079
data = 1
buffer #15:
time = 1000010392299
data = 1
buffer #16:
time = 1000010415519
data = 1
buffer #17:
time = 1000010438739
data = 1
buffer #18:
time = 1000010461959
data = 1
buffer #19:
time = 1000010485179
data = 1
buffer #20:
time = 1000010508399
data = 1
buffer #21:
time = 1000010531619
data = 1
buffer #22:
time = 1000010554839
data = 1
buffer #23:
time = 1000010578058
data = 1
buffer #24:
time = 1000010601278
data = 1
buffer #25:
time = 1000010624498
data = 1
buffer #26:
time = 1000010647718
data = 1
buffer #27:
time = 1000010670938
data = 1
buffer #28:
time = 1000010694158
data = 1
buffer #29:
time = 1000010717378
data = 1
buffer #30:
time = 1000010740598
data = 1
buffer #31:
time = 1000010763818
data = 1
buffer #32:
time = 1000010787038
data = 1
buffer #33:
time = 1000010810258
data = 1
buffer #34:
time = 1000010833478
data = 1
buffer #35:
time = 1000010856698
data = 1
buffer #36:
time = 1000010879918
data = 1
buffer #37:
time = 1000010903138
data = 1
buffer #38:
time = 1000010926358
data = 1
buffer #39:
time = 1000010949578
data = 1
buffer #40:
time = 1000010972798
data = 1
buffer #41:
time = 1000010996018
data = 1
buffer #42:
time = 1000011019238
data = 1
buffer #43:
time = 1000011042458
data = 1
buffer #44:
time = 1000011065678
data = 1
ImageOutput:
rendered image count = 2
image output #1:
presentationTimeUs = 0
bitmap hash = -389047680
image output #2:
presentationTimeUs = 0
bitmap hash = -389047680