diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/ExoPlayerAssetLoader.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/ExoPlayerAssetLoader.java index 5122c3263a..c8fb3f7c71 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/ExoPlayerAssetLoader.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/ExoPlayerAssetLoader.java @@ -235,7 +235,6 @@ public final class ExoPlayerAssetLoader implements AssetLoader { public void start() { player.setMediaItem(mediaItem); player.prepare(); - player.play(); progressState = PROGRESS_STATE_WAITING_FOR_AVAILABILITY; } @@ -355,9 +354,13 @@ public final class ExoPlayerAssetLoader implements AssetLoader { "The asset loader has no track to output.", /* cause= */ null, ERROR_CODE_FAILED_RUNTIME_CHECK)); + return; } else { assetLoaderListener.onTrackCount(trackCount); } + // Start the renderers after having registered all the tracks to make sure the AssetLoader + // listener callbacks are called in the right order. + player.play(); } @Override diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/ExoPlayerAssetLoaderTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/ExoPlayerAssetLoaderTest.java new file mode 100644 index 0000000000..9d1068c732 --- /dev/null +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/ExoPlayerAssetLoaderTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2022 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 com.google.android.exoplayer2.transformer; + +import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runLooperUntil; +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.util.Clock; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowSystemClock; + +/** Unit tests for {@link ExoPlayerAssetLoader}. */ +@RunWith(AndroidJUnit4.class) +public class ExoPlayerAssetLoaderTest { + + @Test + public void exoPlayerAssetLoader_callsListenerCallbacksInRightOrder() throws Exception { + HandlerThread assetLoaderThread = new HandlerThread("AssetLoaderThread"); + assetLoaderThread.start(); + Looper assetLoaderLooper = assetLoaderThread.getLooper(); + AtomicReference exceptionRef = new AtomicReference<>(); + AtomicBoolean isTrackAdded = new AtomicBoolean(); + AssetLoader.Listener listener = + new AssetLoader.Listener() { + + private volatile boolean isDurationSet; + private volatile boolean isTrackCountSet; + + @Override + public void onDurationUs(long durationUs) { + // Sleep to increase the chances of the test failing. + sleep(); + isDurationSet = true; + } + + @Override + public void onTrackCount(int trackCount) { + // Sleep to increase the chances of the test failing. + sleep(); + isTrackCountSet = true; + } + + @Override + public SamplePipeline.Input onTrackAdded( + Format format, + @AssetLoader.SupportedOutputTypes int supportedOutputTypes, + long streamStartPositionUs, + long streamOffsetUs) { + if (!isDurationSet) { + exceptionRef.set( + new IllegalStateException("onTrackAdded() called before onDurationUs()")); + } else if (!isTrackCountSet) { + exceptionRef.set( + new IllegalStateException("onTrackAdded() called before onTrackCount()")); + } + isTrackAdded.set(true); + return new FakeSamplePipelineInput(); + } + + @Override + public void onError(Exception e) { + exceptionRef.set(e); + } + + private void sleep() { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + exceptionRef.set(e); + } + } + }; + // Use default clock so that messages sent on different threads are not always executed in the + // order in which they are received. + Clock clock = Clock.DEFAULT; + AssetLoader assetLoader = getAssetLoader(assetLoaderLooper, listener, clock); + + new Handler(assetLoaderLooper).post(assetLoader::start); + runLooperUntil( + Looper.myLooper(), + () -> { + ShadowSystemClock.advanceBy(Duration.ofMillis(10)); + return isTrackAdded.get() || exceptionRef.get() != null; + }); + + assertThat(exceptionRef.get()).isNull(); + } + + private static AssetLoader getAssetLoader( + Looper looper, AssetLoader.Listener listener, Clock clock) { + Context context = ApplicationProvider.getApplicationContext(); + MediaItem mediaItem = MediaItem.fromUri("asset:///media/mp4/sample.mp4"); + return new ExoPlayerAssetLoader.Factory() + .setContext(context) + .setMediaItem(mediaItem) + .setRemoveAudio(false) + .setRemoveVideo(false) + .setFlattenVideoForSlowMotion(false) + .setDecoderFactory(new DefaultDecoderFactory(context)) + .setLooper(looper) + .setListener(listener) + .setClock(clock) + .createAssetLoader(); + } + + private static final class FakeSamplePipelineInput implements SamplePipeline.Input { + + @Override + public boolean expectsDecodedData() { + return false; + } + + @Nullable + @Override + public DecoderInputBuffer dequeueInputBuffer() { + return null; + } + + @Override + public void queueInputBuffer() {} + } +}