From e01ef47db9c5cf8d75205d3fe5a99867f5ecf5e1 Mon Sep 17 00:00:00 2001 From: huangdarwin Date: Fri, 10 Dec 2021 16:41:27 +0000 Subject: [PATCH 01/56] Use static asserts more often. PiperOrigin-RevId: 415529751 --- .../android/exoplayer2/transformer/mh/AndroidTestUtil.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/AndroidTestUtil.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/AndroidTestUtil.java index 6e9512a020..6634d4fd8f 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/AndroidTestUtil.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/AndroidTestUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.transformer.mh; +import static com.google.android.exoplayer2.util.Assertions.checkState; import static com.google.common.truth.Truth.assertWithMessage; import static java.util.concurrent.TimeUnit.SECONDS; @@ -25,7 +26,6 @@ import androidx.annotation.Nullable; import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.transformer.Transformer; -import com.google.android.exoplayer2.util.Assertions; import java.io.File; import java.io.FileWriter; import java.io.IOException; @@ -135,9 +135,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private static File createExternalCacheFile(Context context, String fileName) throws IOException { File file = new File(context.getExternalCacheDir(), fileName); - Assertions.checkState( - !file.exists() || file.delete(), "Could not delete file: " + file.getAbsolutePath()); - Assertions.checkState(file.createNewFile(), "Could not create file: " + file.getAbsolutePath()); + checkState(!file.exists() || file.delete(), "Could not delete file: " + file.getAbsolutePath()); + checkState(file.createNewFile(), "Could not create file: " + file.getAbsolutePath()); return file; } From 7fca1a0876c3f43f3e36f383f46c7575b13bc0df Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 13 Dec 2021 09:45:15 +0000 Subject: [PATCH 02/56] Make DecoderCountersUtil error message clearer By including the full counters in the failure message we have a clearer insight into the cause of the failure. PiperOrigin-RevId: 415982732 --- .../exoplayer2/decoder/DecoderCounters.java | 32 ++++++++++ .../testutil/DecoderCountersUtil.java | 59 ++++++------------- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java index 48cddf2d50..0af4ff029f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2.decoder; import static java.lang.Math.max; +import com.google.android.exoplayer2.util.Util; + /** * Maintains decoder event counts, for debugging purposes only. * @@ -154,4 +156,34 @@ public final class DecoderCounters { totalVideoFrameProcessingOffsetUs += totalProcessingOffsetUs; videoFrameProcessingOffsetCount += count; } + + @Override + public String toString() { + return Util.formatInvariant( + "DecoderCounters {\n " + + "decoderInits=%s,\n " + + "decoderReleases=%s\n " + + "queuedInputBuffers=%s\n " + + "skippedInputBuffers=%s\n " + + "renderedOutputBuffers=%s\n " + + "skippedOutputBuffers=%s\n " + + "droppedBuffers=%s\n " + + "droppedInputBuffers=%s\n " + + "maxConsecutiveDroppedBuffers=%s\n " + + "droppedToKeyframeEvents=%s\n " + + "totalVideoFrameProcessingOffsetUs=%s\n " + + "videoFrameProcessingOffsetCount=%s\n}", + decoderInitCount, + decoderReleaseCount, + queuedInputBufferCount, + skippedInputBufferCount, + renderedOutputBufferCount, + skippedOutputBufferCount, + droppedBufferCount, + droppedInputBufferCount, + maxConsecutiveDroppedBufferCount, + droppedToKeyframeCount, + totalVideoFrameProcessingOffsetUs, + videoFrameProcessingOffsetCount); + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java index 2e6934a8c8..99e583aa3d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java @@ -43,7 +43,7 @@ public final class DecoderCountersUtil { counters.ensureUpdated(); int actual = counters.skippedOutputBufferCount; assertWithMessage( - "Codec(" + name + ") skipped " + actual + " buffers. Expected " + expected + ".") + "Codec(%s) skipped an unexpected number of buffers. Counters:\n%s", name, counters) .that(actual) .isEqualTo(expected); } @@ -66,32 +66,19 @@ public final class DecoderCountersUtil { public static void assertTotalBufferCount( String name, DecoderCounters counters, int minCount, int maxCount) { int actual = getTotalBufferCount(counters); - assertWithMessage( - "Codec(" - + name - + ") output " - + actual - + " buffers. Expected in range [" - + minCount - + ", " - + maxCount - + "].") - .that(minCount <= actual && actual <= maxCount) - .isTrue(); + assertWithMessage("Codec(%s) output too few buffers. Counters:\n%s", name, counters) + .that(actual) + .isAtLeast(minCount); + assertWithMessage("Codec(%s) output too many buffers. Counters:\n%s", name, counters) + .that(actual) + .isAtMost(maxCount); } public static void assertDroppedBufferLimit(String name, DecoderCounters counters, int limit) { counters.ensureUpdated(); int actual = counters.droppedBufferCount; assertWithMessage( - "Codec(" - + name - + ") was late decoding: " - + actual - + " buffers. " - + "Limit: " - + limit - + ".") + "Codec(%s) was late decoding too many buffers. Counters:\n%s: ", name, counters) .that(actual) .isAtMost(limit); } @@ -101,14 +88,8 @@ public final class DecoderCountersUtil { counters.ensureUpdated(); int actual = counters.maxConsecutiveDroppedBufferCount; assertWithMessage( - "Codec(" - + name - + ") was late decoding: " - + actual - + " buffers consecutively. " - + "Limit: " - + limit - + ".") + "Codec(%s) was late decoding too many buffers consecutively. Counters:\n%s", + name, counters) .that(actual) .isAtMost(limit); } @@ -117,16 +98,14 @@ public final class DecoderCountersUtil { String name, DecoderCounters counters, int minCount, int maxCount) { int actual = counters.videoFrameProcessingOffsetCount; assertWithMessage( - "Codec(" - + name - + ") videoFrameProcessingOffsetSampleCount " - + actual - + ". Expected in range [" - + minCount - + ", " - + maxCount - + "].") - .that(minCount <= actual && actual <= maxCount) - .isTrue(); + "Codec(%s) videoFrameProcessingOffsetSampleCount too low. Counters:\n%s", + name, counters) + .that(actual) + .isAtLeast(minCount); + assertWithMessage( + "Codec(%s) videoFrameProcessingOffsetSampleCount too high. Counters:\n%s", + name, counters) + .that(actual) + .isAtMost(maxCount); } } From 3e098f34e2f8eb8dde96d97d68309d1a8606c274 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 13 Dec 2021 10:13:52 +0000 Subject: [PATCH 03/56] Bump Guava version to 31.0.1 The version in the android tree was updated in https://cs.android.com/android/_/android/platform/external/guava/+/8337762fe0ca3a07056d30318a7df2444c1a0284 PiperOrigin-RevId: 415988097 --- constants.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.gradle b/constants.gradle index 7fd89a86d6..853ff6ff20 100644 --- a/constants.gradle +++ b/constants.gradle @@ -25,7 +25,7 @@ project.ext { junitVersion = '4.13.2' // Use the same Guava version as the Android repo: // https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA - guavaVersion = '27.1-android' + guavaVersion = '31.0.1-android' mockitoVersion = '3.12.4' robolectricVersion = '4.6.1' // Keep this in sync with Google's internal Checker Framework version. From e148a678ac5fc42e1a87a90bf4e99ad04968818e Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 13 Dec 2021 11:32:37 +0000 Subject: [PATCH 04/56] Revert 3e098f34e2f8eb8dde96d97d68309d1a8606c274 Breaks the gradle build PiperOrigin-RevId: 415999600 --- constants.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.gradle b/constants.gradle index 853ff6ff20..7fd89a86d6 100644 --- a/constants.gradle +++ b/constants.gradle @@ -25,7 +25,7 @@ project.ext { junitVersion = '4.13.2' // Use the same Guava version as the Android repo: // https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA - guavaVersion = '31.0.1-android' + guavaVersion = '27.1-android' mockitoVersion = '3.12.4' robolectricVersion = '4.6.1' // Keep this in sync with Google's internal Checker Framework version. From e8ee6dad2a47eeae4e64574e44c6db7eed82c512 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 13 Dec 2021 12:42:11 +0000 Subject: [PATCH 05/56] Revert e148a678ac5fc42e1a87a90bf4e99ad04968818e Fix the gradle problem PiperOrigin-RevId: 416011494 --- constants.gradle | 2 +- library/common/build.gradle | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/constants.gradle b/constants.gradle index 7fd89a86d6..853ff6ff20 100644 --- a/constants.gradle +++ b/constants.gradle @@ -25,7 +25,7 @@ project.ext { junitVersion = '4.13.2' // Use the same Guava version as the Android repo: // https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA - guavaVersion = '27.1-android' + guavaVersion = '31.0.1-android' mockitoVersion = '3.12.4' robolectricVersion = '4.6.1' // Keep this in sync with Google's internal Checker Framework version. diff --git a/library/common/build.gradle b/library/common/build.gradle index b59552d366..871bf5d8a4 100644 --- a/library/common/build.gradle +++ b/library/common/build.gradle @@ -27,6 +27,7 @@ dependencies { // (but declared as runtime deps) [internal b/168188131]. exclude group: 'com.google.code.findbugs', module: 'jsr305' exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'org.checkerframework', module: 'checker-qual' exclude group: 'com.google.errorprone', module: 'error_prone_annotations' exclude group: 'com.google.j2objc', module: 'j2objc-annotations' exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' From 95a750cefd73565f77bd19338735ad994e8ca150 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 13 Dec 2021 12:56:55 +0000 Subject: [PATCH 06/56] Change DefaultHttpDataSourceTest to an instrumentation test The Robolectric implementation of HttpURLConnection forwards to the JRE implementation [1], which behaves differently to the Android one available on devices and emulators. For these tests to be a realistic test of the HTTP stack used in real playbacks we can't use Robolectric. Similar to https://github.com/google/ExoPlayer/commit/df0e89c1678ff0dda00bb187be05b8198bd31567 [1] https://github.com/robolectric/robolectric/issues/6769#issuecomment-943556156 PiperOrigin-RevId: 416013662 --- library/datasource/build.gradle | 1 + .../android/exoplayer2/upstream/DefaultHttpDataSourceTest.java | 0 2 files changed, 1 insertion(+) rename library/datasource/src/{test => androidTest}/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java (100%) diff --git a/library/datasource/build.gradle b/library/datasource/build.gradle index 4bd29fb1c2..27f35ed811 100644 --- a/library/datasource/build.gradle +++ b/library/datasource/build.gradle @@ -42,6 +42,7 @@ dependencies { androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion + androidTestImplementation 'com.squareup.okhttp3:mockwebserver:' + okhttpVersion androidTestImplementation(project(modulePrefix + 'testutils')) { exclude module: modulePrefix.substring(1) + 'library-core' } diff --git a/library/datasource/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java b/library/datasource/src/androidTest/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java similarity index 100% rename from library/datasource/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java rename to library/datasource/src/androidTest/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java From 40bd99a3cd2792c922b62b0acc39b4a5fe2a69fa Mon Sep 17 00:00:00 2001 From: christosts Date: Mon, 13 Dec 2021 14:25:18 +0000 Subject: [PATCH 07/56] Load the Spatializer API with reflection This change adds a delegate class that loads and forwards calls to a Spatializer with reflection, so that we can use the Spatializer API before we update the compile SDK target to 32. PiperOrigin-RevId: 416027289 --- .../exoplayer2/audio/SpatializerDelegate.java | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/audio/SpatializerDelegate.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SpatializerDelegate.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SpatializerDelegate.java new file mode 100644 index 0000000000..9af179132d --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SpatializerDelegate.java @@ -0,0 +1,217 @@ +/* + * Copyright 2021 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.audio; + +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; + +import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioFormat; +import android.media.AudioManager; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * Exposes the android.media.Spatializer API via reflection. This is so that we can use the + * Spatializer while the compile SDK target is set to 31. + */ +@RequiresApi(31) +/* package */ final class SpatializerDelegate { + /** Level of support for audio spatialization. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) + @IntDef({ + SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL, + SPATIALIZER_IMMERSIVE_LEVEL_NONE, + SPATIALIZER_IMMERSIVE_LEVEL_OTHER + }) + @interface ImmersiveAudioLevel {} + + /** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL */ + public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1; + /** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_NONE */ + public static final int SPATIALIZER_IMMERSIVE_LEVEL_NONE = 0; + /** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_OTHER */ + public static final int SPATIALIZER_IMMERSIVE_LEVEL_OTHER = -1; + + /** Wrapper for Spatializer.OnSpatializerStateChangedListener */ + public interface Listener { + /** See Spatializer.OnSpatializerStateChangedListener.onSpatializerEnabledChanged */ + void onSpatializerEnabledChanged(SpatializerDelegate spatializer, boolean enabled); + + /** See Spatializer.OnSpatializerStateChangedListener.onSpatializerAvailableChanged */ + void onSpatializerAvailableChanged(SpatializerDelegate spatializer, boolean available); + } + + private final Object spatializer; + private final Class spatializerClass; + private final Class spatializerListenerClass; + private final Method isEnabled; + private final Method isAvailable; + private final Method getImmersiveAudioLevel; + private final Method canBeSpatialized; + private final Method addListener; + private final Method removeListener; + private final Map listeners; + + /** Creates an instance. */ + public SpatializerDelegate(Context context) + throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, + IllegalAccessException { + Method getSpatializerMethod = AudioManager.class.getMethod("getSpatializer"); + AudioManager manager = + Assertions.checkNotNull( + (AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE)); + spatializer = checkStateNotNull(getSpatializerMethod.invoke(manager)); + spatializerClass = Class.forName("android.media.Spatializer"); + spatializerListenerClass = + Class.forName("android.media.Spatializer$OnSpatializerStateChangedListener"); + isEnabled = spatializerClass.getMethod("isEnabled"); + isAvailable = spatializerClass.getMethod("isAvailable"); + getImmersiveAudioLevel = spatializerClass.getMethod("getImmersiveAudioLevel"); + canBeSpatialized = + spatializerClass.getMethod( + "canBeSpatialized", android.media.AudioAttributes.class, AudioFormat.class); + addListener = + spatializerClass.getMethod( + "addOnSpatializerStateChangedListener", Executor.class, spatializerListenerClass); + removeListener = + spatializerClass.getMethod( + "removeOnSpatializerStateChangedListener", spatializerListenerClass); + listeners = new HashMap<>(); + } + + /** Delegates to Spatializer.isEnabled() */ + public boolean isEnabled() { + try { + return (boolean) Util.castNonNull(isEnabled.invoke(spatializer)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + + /** Delegates to Spatializer.isAvailable() */ + public boolean isAvailable() { + try { + return (boolean) Util.castNonNull(isAvailable.invoke(spatializer)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + + /** Delegates to Spatializer.getImmersiveAudioLevel() */ + @ImmersiveAudioLevel + public int getImmersiveAudioLevel() { + try { + return (int) Util.castNonNull(getImmersiveAudioLevel.invoke(spatializer)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + + /** Delegates to Spatializer.canBeSpatialized() */ + public boolean canBeSpatialized(AudioAttributes attributes, AudioFormat format) { + try { + return (boolean) Util.castNonNull(canBeSpatialized.invoke(spatializer, attributes, format)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + + /** Delegates to Spatializer.addOnSpatializerStateChangedListener() */ + public void addOnSpatializerStateChangedListener(Executor executor, Listener listener) { + if (listeners.containsKey(listener)) { + return; + } + Object listenerProxy = createSpatializerListenerProxy(listener); + try { + addListener.invoke(spatializer, executor, listenerProxy); + listeners.put(listener, listenerProxy); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + + /** Delegates to Spatializer.removeOnSpatializerStateChangedListener() */ + public void removeOnSpatializerStateChangedListener(Listener listener) { + @Nullable Object proxy = listeners.get(listener); + if (proxy == null) { + return; + } + try { + removeListener.invoke(spatializer, proxy); + listeners.remove(listener); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + + private Object createSpatializerListenerProxy(Listener listener) { + return Proxy.newProxyInstance( + spatializerListenerClass.getClassLoader(), + new Class[] {spatializerListenerClass}, + new ProxySpatializerListener(this, listener)); + } + + /** Proxy-based implementation of Spatializer.OnSpatializerStateChangedListener. */ + private static final class ProxySpatializerListener implements InvocationHandler { + private final SpatializerDelegate spatializerDelegate; + private final Listener listener; + + private ProxySpatializerListener(SpatializerDelegate spatializerDelegate, Listener listener) { + this.spatializerDelegate = spatializerDelegate; + this.listener = listener; + } + + @Override + public Object invoke(Object o, Method method, Object[] objects) { + String methodName = method.getName(); + Class[] parameterTypes = method.getParameterTypes(); + if (methodName.equals("onSpatializerAvailableChanged") + && parameterTypes.length == 2 + && spatializerDelegate.spatializerClass.isAssignableFrom(parameterTypes[0]) + && parameterTypes[1].equals(Boolean.TYPE)) { + listener.onSpatializerAvailableChanged(spatializerDelegate, (boolean) objects[1]); + } else if (methodName.equals("onSpatializerEnabledChanged") + && parameterTypes.length == 2 + && spatializerDelegate.spatializerClass.isAssignableFrom(parameterTypes[0]) + && parameterTypes[1].equals(Boolean.TYPE)) { + listener.onSpatializerEnabledChanged(spatializerDelegate, (boolean) objects[1]); + } + return this; + } + } +} From 9cdcc58770e596133bb43247e610c2bee909c863 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Mon, 13 Dec 2021 15:00:43 +0000 Subject: [PATCH 08/56] Add TransformationException with initial subset of error codes. TransformationException will be used for all errors that occur during a transformation. PiperOrigin-RevId: 416032504 --- .../transformer/TransformationException.java | 201 ++++++++++++++++++ .../exoplayer2/transformer/Transformer.java | 27 ++- .../transformer/TransformerTest.java | 8 +- 3 files changed, 227 insertions(+), 9 deletions(-) create mode 100644 library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java new file mode 100644 index 0000000000..705a026f34 --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java @@ -0,0 +1,201 @@ +/* + * Copyright 2021 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 java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; + +import android.os.SystemClock; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Thrown when a non-locally recoverable transformation failure occurs. */ +public final class TransformationException extends Exception { + + /** + * Creates an instance for an unexpected exception. + * + *

If the exception is a runtime exception, error code {@link ERROR_CODE_FAILED_RUNTIME_CHECK} + * is used. Otherwise, the created instance has error code {@link ERROR_CODE_UNSPECIFIED}. + * + * @param cause The cause of the failure. + * @return The created instance. + */ + public static TransformationException createForUnexpected(Exception cause) { + if (cause instanceof RuntimeException) { + return new TransformationException( + "Unexpected runtime error", cause, ERROR_CODE_FAILED_RUNTIME_CHECK); + } + return new TransformationException("Unexpected error", cause, ERROR_CODE_UNSPECIFIED); + } + + /** + * Codes that identify causes of {@link Transformer} errors. + * + *

This list of errors may be extended in future versions. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) + @IntDef( + open = true, + value = { + ERROR_CODE_UNSPECIFIED, + ERROR_CODE_FAILED_RUNTIME_CHECK, + ERROR_CODE_DECODER_INIT_FAILED, + ERROR_CODE_DECODING_FAILED, + ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, + ERROR_CODE_ENCODER_INIT_FAILED, + ERROR_CODE_ENCODING_FAILED, + ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED, + ERROR_CODE_GL_INIT_FAILED, + ERROR_CODE_GL_PROCESSING_FAILED, + }) + public @interface ErrorCode {} + + // Miscellaneous errors (1xxx). + + /** Caused by an error whose cause could not be identified. */ + public static final int ERROR_CODE_UNSPECIFIED = 1000; + /** + * Caused by a failed runtime check. + * + *

This can happen when transformer reaches an invalid state. + */ + public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 1001; + + // Decoding errors (2xxx). + + /** Caused by a decoder initialization failure. */ + public static final int ERROR_CODE_DECODER_INIT_FAILED = 2001; + /** Caused by a failure while trying to decode media samples. */ + public static final int ERROR_CODE_DECODING_FAILED = 2002; + /** Caused by trying to decode content whose format is not supported. */ + public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 2003; + + // Encoding errors (3xxx). + + /** Caused by an encoder initialization failure. */ + public static final int ERROR_CODE_ENCODER_INIT_FAILED = 3001; + /** Caused by a failure while trying to encode media samples. */ + public static final int ERROR_CODE_ENCODING_FAILED = 3002; + /** Caused by requesting to encode content in a format that is not supported by the device. */ + public static final int ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED = 3003; + + // GL errors (4xxx). + + /** Caused by a GL initialization failure. */ + public static final int ERROR_CODE_GL_INIT_FAILED = 4001; + /** Caused by a failure while using or releasing a GL program. */ + public static final int ERROR_CODE_GL_PROCESSING_FAILED = 4002; + + /** Returns the name of a given {@code errorCode}. */ + public static String getErrorCodeName(@ErrorCode int errorCode) { + switch (errorCode) { + case ERROR_CODE_UNSPECIFIED: + return "ERROR_CODE_UNSPECIFIED"; + case ERROR_CODE_FAILED_RUNTIME_CHECK: + return "ERROR_CODE_FAILED_RUNTIME_CHECK"; + case ERROR_CODE_DECODER_INIT_FAILED: + return "ERROR_CODE_DECODER_INIT_FAILED"; + case ERROR_CODE_DECODING_FAILED: + return "ERROR_CODE_DECODING_FAILED"; + case ERROR_CODE_DECODING_FORMAT_UNSUPPORTED: + return "ERROR_CODE_DECODING_FORMAT_UNSUPPORTED"; + case ERROR_CODE_ENCODER_INIT_FAILED: + return "ERROR_CODE_ENCODER_INIT_FAILED"; + case ERROR_CODE_ENCODING_FAILED: + return "ERROR_CODE_ENCODING_FAILED"; + case ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED: + return "ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED"; + case ERROR_CODE_GL_INIT_FAILED: + return "ERROR_CODE_GL_INIT_FAILED"; + case ERROR_CODE_GL_PROCESSING_FAILED: + return "ERROR_CODE_GL_PROCESSING_FAILED"; + default: + return "invalid error code"; + } + } + + /** + * Equivalent to {@link TransformationException#getErrorCodeName(int) + * TransformationException.getErrorCodeName(this.errorCode)}. + */ + public final String getErrorCodeName() { + return getErrorCodeName(errorCode); + } + + /** An error code which identifies the cause of the transformation failure. */ + public final @ErrorCode int errorCode; + + /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */ + public final long timestampMs; + + /** + * Creates an instance. + * + * @param message See {@link #getMessage()}. + * @param cause See {@link #getCause()}. + * @param errorCode A number which identifies the cause of the error. May be one of the {@link + * ErrorCode ErrorCodes}. + */ + public TransformationException( + @Nullable String message, @Nullable Throwable cause, @ErrorCode int errorCode) { + super(message, cause); + this.errorCode = errorCode; + this.timestampMs = Clock.DEFAULT.elapsedRealtime(); + } + + /** + * Returns whether the error data associated to this exception equals the error data associated to + * {@code other}. + * + *

Note that this method does not compare the exceptions' stacktraces. + */ + public boolean errorInfoEquals(@Nullable TransformationException other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + + @Nullable Throwable thisCause = getCause(); + @Nullable Throwable thatCause = other.getCause(); + if (thisCause != null && thatCause != null) { + if (!Util.areEqual(thisCause.getMessage(), thatCause.getMessage())) { + return false; + } + if (!Util.areEqual(thisCause.getClass(), thatCause.getClass())) { + return false; + } + } else if (thisCause != null || thatCause != null) { + return false; + } + return errorCode == other.errorCode + && Util.areEqual(getMessage(), other.getMessage()) + && timestampMs == other.timestampMs; + } +} diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java index 71dc7bd77c..ab52a5cf40 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java @@ -832,22 +832,37 @@ public final class Transformer { @Override public void onPlayerError(PlaybackException error) { + // TODO(internal b/209469847): Once TransformationException is used in transformer components, + // extract TransformationExceptions wrapped in the PlaybackExceptions here before passing them + // on. handleTransformationEnded(error); } private void handleTransformationEnded(@Nullable Exception exception) { + @Nullable Exception resourceReleaseException = null; try { releaseResources(/* forCancellation= */ false); } catch (IllegalStateException e) { - if (exception == null) { - exception = e; - } + // TODO(internal b/209469847): Use a TransformationException with a specific error code when + // the IllegalStateException is caused by the muxer. + resourceReleaseException = e; } - if (exception == null) { + if (exception == null && resourceReleaseException == null) { listener.onTransformationCompleted(mediaItem); - } else { - listener.onTransformationError(mediaItem, exception); + return; + } + + if (exception != null) { + listener.onTransformationError( + mediaItem, + exception instanceof TransformationException + ? exception + : TransformationException.createForUnexpected(exception)); + } + if (resourceReleaseException != null) { + listener.onTransformationError( + mediaItem, TransformationException.createForUnexpected(resourceReleaseException)); } } } diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java index 4fea0e999d..e477c2140c 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java @@ -240,8 +240,9 @@ public final class TransformerTest { transformer.startTransformation(mediaItem, outputPath); Exception exception = TransformerTestRunner.runUntilError(transformer); - assertThat(exception).isInstanceOf(ExoPlaybackException.class); - assertThat(exception).hasCauseThat().isInstanceOf(IOException.class); + assertThat(exception).isInstanceOf(TransformationException.class); + assertThat(exception).hasCauseThat().isInstanceOf(ExoPlaybackException.class); + assertThat(exception).hasCauseThat().hasCauseThat().isInstanceOf(IOException.class); } @Test @@ -253,7 +254,8 @@ public final class TransformerTest { transformer.startTransformation(mediaItem, outputPath); Exception exception = TransformerTestRunner.runUntilError(transformer); - assertThat(exception).isInstanceOf(IllegalStateException.class); + assertThat(exception).isInstanceOf(TransformationException.class); + assertThat(exception).hasCauseThat().isInstanceOf(IllegalStateException.class); } @Test From bf1224186c5bb9751dbb353cbd9e878129b2c2b0 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 14 Dec 2021 01:12:47 +0000 Subject: [PATCH 09/56] Rollback of https://github.com/google/ExoPlayer/commit/0aa23b08b1624cacc9414cd093ff64ca1e0b1a10 *** Original commit *** Add capability flags for hardware and decoder support Issue: google/ExoPlayer#9565 *** PiperOrigin-RevId: 416170329 --- .../exoplayer2/RendererCapabilities.java | 141 ++---------------- .../audio/MediaCodecAudioRenderer.java | 20 +-- .../trackselection/MappingTrackSelector.java | 15 +- .../android/exoplayer2/util/EventLogger.java | 26 +--- .../video/MediaCodecVideoRenderer.java | 22 +-- .../audio/DecoderAudioRendererTest.java | 10 +- 6 files changed, 30 insertions(+), 204 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index 654ea54409..62eaa49b9e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -60,16 +60,16 @@ public interface RendererCapabilities { @interface AdaptiveSupport {} /** A mask to apply to {@link Capabilities} to obtain the {@link AdaptiveSupport} only. */ - int ADAPTIVE_SUPPORT_MASK = 0b11 << 3; + int ADAPTIVE_SUPPORT_MASK = 0b11000; /** The {@link Renderer} can seamlessly adapt between formats. */ - int ADAPTIVE_SEAMLESS = 0b10 << 3; + int ADAPTIVE_SEAMLESS = 0b10000; /** * The {@link Renderer} can adapt between formats, but may suffer a brief discontinuity * (~50-100ms) when adaptation occurs. */ - int ADAPTIVE_NOT_SEAMLESS = 0b01 << 3; + int ADAPTIVE_NOT_SEAMLESS = 0b01000; /** The {@link Renderer} does not support adaptation between formats. */ - int ADAPTIVE_NOT_SUPPORTED = 0; + int ADAPTIVE_NOT_SUPPORTED = 0b00000; /** * Level of renderer support for tunneling. One of {@link #TUNNELING_SUPPORTED} or {@link @@ -80,62 +80,20 @@ public interface RendererCapabilities { @IntDef({TUNNELING_SUPPORTED, TUNNELING_NOT_SUPPORTED}) @interface TunnelingSupport {} - /** A mask to apply to {@link Capabilities} to obtain {@link TunnelingSupport} only. */ - int TUNNELING_SUPPORT_MASK = 0b1 << 5; + /** A mask to apply to {@link Capabilities} to obtain the {@link TunnelingSupport} only. */ + int TUNNELING_SUPPORT_MASK = 0b100000; /** The {@link Renderer} supports tunneled output. */ - int TUNNELING_SUPPORTED = 0b1 << 5; + int TUNNELING_SUPPORTED = 0b100000; /** The {@link Renderer} does not support tunneled output. */ - int TUNNELING_NOT_SUPPORTED = 0; - - /** - * Level of renderer support for hardware acceleration. One of {@link - * #HARDWARE_ACCELERATION_SUPPORTED} and {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED}. - * - *

For video renderers, the level of support is indicated for non-tunneled output. - */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - HARDWARE_ACCELERATION_SUPPORTED, - HARDWARE_ACCELERATION_NOT_SUPPORTED, - }) - @interface HardwareAccelerationSupport {} - /** A mask to apply to {@link Capabilities} to obtain {@link HardwareAccelerationSupport} only. */ - int HARDWARE_ACCELERATION_SUPPORT_MASK = 0b1 << 6; - /** The renderer is able to use hardware acceleration. */ - int HARDWARE_ACCELERATION_SUPPORTED = 0b1 << 6; - /** The renderer is not able to use hardware acceleration. */ - int HARDWARE_ACCELERATION_NOT_SUPPORTED = 0; - - /** - * Level of decoder support. One of {@link #DECODER_SUPPORT_PRIMARY} and {@link - * #DECODER_SUPPORT_FALLBACK}. - * - *

For video renderers, the level of support is indicated for non-tunneled output. - */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - DECODER_SUPPORT_PRIMARY, - DECODER_SUPPORT_FALLBACK, - }) - @interface DecoderSupport {} - /** A mask to apply to {@link Capabilities} to obtain {@link DecoderSupport} only. */ - int MODE_SUPPORT_MASK = 0b1 << 7; - /** The renderer is able to use the primary decoder for the format's MIME type. */ - int DECODER_SUPPORT_PRIMARY = 0b1 << 7; - /** The renderer will use a fallback decoder. */ - int DECODER_SUPPORT_FALLBACK = 0; + int TUNNELING_NOT_SUPPORTED = 0b000000; /** * Combined renderer capabilities. * - *

This is a bitwise OR of {@link C.FormatSupport}, {@link AdaptiveSupport}, {@link - * TunnelingSupport}, {@link HardwareAccelerationSupport} and {@link DecoderSupport}. Use {@link - * #getFormatSupport}, {@link #getAdaptiveSupport}, {@link #getTunnelingSupport}, {@link - * #getHardwareAccelerationSupport} and {@link #getDecoderSupport} to obtain individual - * components. Use {@link #create(int)}, {@link #create(int, int, int)} or {@link #create(int, - * int, int, int, int)} to create combined capabilities from individual components. + *

This is a bitwise OR of {@link C.FormatSupport}, {@link AdaptiveSupport} and {@link + * TunnelingSupport}. Use {@link #getFormatSupport(int)}, {@link #getAdaptiveSupport(int)} or + * {@link #getTunnelingSupport(int)} to obtain the individual flags. And use {@link #create(int)} + * or {@link #create(int, int, int)} to create the combined capabilities. * *

Possible values: * @@ -153,11 +111,6 @@ public interface RendererCapabilities { * #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of * support for the format itself is {@link C#FORMAT_HANDLED} or {@link * C#FORMAT_EXCEEDS_CAPABILITIES}. - *

  • {@link HardwareAccelerationSupport}: The level of support for hardware acceleration. One - * of {@link #HARDWARE_ACCELERATION_SUPPORTED} and {@link - * #HARDWARE_ACCELERATION_NOT_SUPPORTED}. - *
  • {@link DecoderSupport}: The level of decoder support. One of {@link - * #DECODER_SUPPORT_PRIMARY} and {@link #DECODER_SUPPORT_FALLBACK}. * */ @Documented @@ -169,10 +122,8 @@ public interface RendererCapabilities { /** * Returns {@link Capabilities} for the given {@link C.FormatSupport}. * - *

    {@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED}, {@link TunnelingSupport} - * is set to {@link #TUNNELING_NOT_SUPPORTED}, {@link HardwareAccelerationSupport} is set to - * {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED} and {@link DecoderSupport} is set to {@link - * #DECODER_SUPPORT_PRIMARY}. + *

    The {@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED} and {{@link + * TunnelingSupport} is set to {@link #TUNNELING_NOT_SUPPORTED}. * * @param formatSupport The {@link C.FormatSupport}. * @return The combined {@link Capabilities} of the given {@link C.FormatSupport}, {@link @@ -187,53 +138,19 @@ public interface RendererCapabilities { * Returns {@link Capabilities} combining the given {@link C.FormatSupport}, {@link * AdaptiveSupport} and {@link TunnelingSupport}. * - *

    {@link HardwareAccelerationSupport} is set to {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED} - * and {@link DecoderSupport} is set to {@link #DECODER_SUPPORT_PRIMARY}. - * * @param formatSupport The {@link C.FormatSupport}. * @param adaptiveSupport The {@link AdaptiveSupport}. * @param tunnelingSupport The {@link TunnelingSupport}. * @return The combined {@link Capabilities}. */ - @Capabilities - static int create( - @C.FormatSupport int formatSupport, - @AdaptiveSupport int adaptiveSupport, - @TunnelingSupport int tunnelingSupport) { - return create( - formatSupport, - adaptiveSupport, - tunnelingSupport, - HARDWARE_ACCELERATION_NOT_SUPPORTED, - DECODER_SUPPORT_PRIMARY); - } - - /** - * Returns {@link Capabilities} combining the given {@link C.FormatSupport}, {@link - * AdaptiveSupport}, {@link TunnelingSupport}, {@link HardwareAccelerationSupport} and {@link - * DecoderSupport}. - * - * @param formatSupport The {@link C.FormatSupport}. - * @param adaptiveSupport The {@link AdaptiveSupport}. - * @param tunnelingSupport The {@link TunnelingSupport}. - * @param hardwareAccelerationSupport The {@link HardwareAccelerationSupport}. - * @param decoderSupport The {@link DecoderSupport}. - * @return The combined {@link Capabilities}. - */ // Suppression needed for IntDef casting. @SuppressLint("WrongConstant") @Capabilities static int create( @C.FormatSupport int formatSupport, @AdaptiveSupport int adaptiveSupport, - @TunnelingSupport int tunnelingSupport, - @HardwareAccelerationSupport int hardwareAccelerationSupport, - @DecoderSupport int decoderSupport) { - return formatSupport - | adaptiveSupport - | tunnelingSupport - | hardwareAccelerationSupport - | decoderSupport; + @TunnelingSupport int tunnelingSupport) { + return formatSupport | adaptiveSupport | tunnelingSupport; } /** @@ -275,32 +192,6 @@ public interface RendererCapabilities { return supportFlags & TUNNELING_SUPPORT_MASK; } - /** - * Returns the {@link HardwareAccelerationSupport} from the combined {@link Capabilities}. - * - * @param supportFlags The combined {@link Capabilities}. - * @return The {@link HardwareAccelerationSupport} only. - */ - // Suppression needed for IntDef casting. - @SuppressLint("WrongConstant") - @HardwareAccelerationSupport - static int getHardwareAccelerationSupport(@Capabilities int supportFlags) { - return supportFlags & HARDWARE_ACCELERATION_SUPPORT_MASK; - } - - /** - * Returns the {@link DecoderSupport} from the combined {@link Capabilities}. - * - * @param supportFlags The combined {@link Capabilities}. - * @return The {@link DecoderSupport} only. - */ - // Suppression needed for IntDef casting. - @SuppressLint("WrongConstant") - @DecoderSupport - static int getDecoderSupport(@Capabilities int supportFlags) { - return supportFlags & MODE_SUPPORT_MASK; - } - /** Returns the name of the {@link Renderer}. */ String getName(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 41234fa828..fc9a102406 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -314,7 +314,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media // format's MIME type, according to the MediaCodecSelector. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); - boolean isPreferredDecoder = true; if (!isFormatSupported) { // Check whether any of the other decoders support the format. for (int i = 1; i < decoderInfos.size(); i++) { @@ -322,31 +321,18 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media if (otherDecoderInfo.isFormatSupported(format)) { decoderInfo = otherDecoderInfo; isFormatSupported = true; - isPreferredDecoder = false; break; } } } - @C.FormatSupport - int formatSupport = isFormatSupported ? C.FORMAT_HANDLED : C.FORMAT_EXCEEDS_CAPABILITIES; @AdaptiveSupport int adaptiveSupport = isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format) ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; - @HardwareAccelerationSupport - int hardwareAccelerationSupport = - decoderInfo.hardwareAccelerated - ? HARDWARE_ACCELERATION_SUPPORTED - : HARDWARE_ACCELERATION_NOT_SUPPORTED; - @DecoderSupport - int decoderSupport = isPreferredDecoder ? DECODER_SUPPORT_PRIMARY : DECODER_SUPPORT_FALLBACK; - return RendererCapabilities.create( - formatSupport, - adaptiveSupport, - tunnelingSupport, - hardwareAccelerationSupport, - decoderSupport); + @C.FormatSupport + int formatSupport = isFormatSupported ? C.FORMAT_HANDLED : C.FORMAT_EXCEEDS_CAPABILITIES; + return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 37040c944f..226e7b24ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -216,19 +216,6 @@ public abstract class MappingTrackSelector extends TrackSelector { return bestRendererSupport; } - /** - * Returns the {@link Capabilities} of the renderer for an individual track. - * - * @param rendererIndex The renderer index. - * @param groupIndex The index of the track group to which the track belongs. - * @param trackIndex The index of the track within the track group. - * @return The {@link Capabilities}. - */ - @Capabilities - public int getCapabilities(int rendererIndex, int groupIndex, int trackIndex) { - return rendererFormatSupports[rendererIndex][groupIndex][trackIndex]; - } - /** * Returns the extent to which an individual track is supported by the renderer. * @@ -240,7 +227,7 @@ public abstract class MappingTrackSelector extends TrackSelector { @FormatSupport public int getTrackSupport(int rendererIndex, int groupIndex, int trackIndex) { return RendererCapabilities.getFormatSupport( - getCapabilities(rendererIndex, groupIndex, trackIndex)); + rendererFormatSupports[rendererIndex][groupIndex][trackIndex]); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index 00eb88cb80..565f3eb880 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -15,12 +15,6 @@ */ package com.google.android.exoplayer2.util; -import static com.google.android.exoplayer2.RendererCapabilities.DECODER_SUPPORT_FALLBACK; -import static com.google.android.exoplayer2.RendererCapabilities.HARDWARE_ACCELERATION_SUPPORTED; -import static com.google.android.exoplayer2.RendererCapabilities.getDecoderSupport; -import static com.google.android.exoplayer2.RendererCapabilities.getFormatSupport; -import static com.google.android.exoplayer2.RendererCapabilities.getHardwareAccelerationSupport; -import static com.google.android.exoplayer2.util.Util.getFormatSupportString; import static java.lang.Math.min; import android.os.SystemClock; @@ -35,7 +29,6 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; -import com.google.android.exoplayer2.RendererCapabilities.Capabilities; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -283,16 +276,9 @@ public class EventLogger implements AnalyticsListener { logd(" Group:" + trackGroup.id + ", adaptive_supported=" + adaptiveSupport + " ["); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); - @Capabilities - int capabilities = - mappedTrackInfo.getCapabilities(rendererIndex, groupIndex, trackIndex); - String formatSupport = getFormatSupportString(getFormatSupport(capabilities)); - String hardwareAccelerationSupport = - getHardwareAccelerationSupport(capabilities) == HARDWARE_ACCELERATION_SUPPORTED - ? ", accelerated=YES" - : ""; - String decoderSupport = - getDecoderSupport(capabilities) == DECODER_SUPPORT_FALLBACK ? ", fallback=YES" : ""; + String formatSupport = + Util.getFormatSupportString( + mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)); logd( " " + status @@ -301,9 +287,7 @@ public class EventLogger implements AnalyticsListener { + ", " + Format.toLogString(trackGroup.getFormat(trackIndex)) + ", supported=" - + formatSupport - + hardwareAccelerationSupport - + decoderSupport); + + formatSupport); } logd(" ]"); } @@ -331,7 +315,7 @@ public class EventLogger implements AnalyticsListener { TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(false); - String formatSupport = getFormatSupportString(C.FORMAT_UNSUPPORTED_TYPE); + String formatSupport = Util.getFormatSupportString(C.FORMAT_UNSUPPORTED_TYPE); logd( " " + status diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 553644fa93..bf4d185f70 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -374,7 +374,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // format's MIME type, according to the MediaCodecSelector. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); - boolean isPreferredDecoder = true; if (!isFormatSupported) { // Check whether any of the other decoders support the format. for (int i = 1; i < decoderInfos.size(); i++) { @@ -382,26 +381,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (otherDecoderInfo.isFormatSupported(format)) { decoderInfo = otherDecoderInfo; isFormatSupported = true; - isPreferredDecoder = false; break; } } } - @C.FormatSupport - int formatSupport = isFormatSupported ? C.FORMAT_HANDLED : C.FORMAT_EXCEEDS_CAPABILITIES; @AdaptiveSupport int adaptiveSupport = decoderInfo.isSeamlessAdaptationSupported(format) ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; - @HardwareAccelerationSupport - int hardwareAccelerationSupport = - decoderInfo.hardwareAccelerated - ? HARDWARE_ACCELERATION_SUPPORTED - : HARDWARE_ACCELERATION_NOT_SUPPORTED; - @DecoderSupport - int decoderSupport = isPreferredDecoder ? DECODER_SUPPORT_PRIMARY : DECODER_SUPPORT_FALLBACK; - @TunnelingSupport int tunnelingSupport = TUNNELING_NOT_SUPPORTED; if (isFormatSupported) { List tunnelingDecoderInfos = @@ -420,13 +408,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } } - - return RendererCapabilities.create( - formatSupport, - adaptiveSupport, - tunnelingSupport, - hardwareAccelerationSupport, - decoderSupport); + @C.FormatSupport + int formatSupport = isFormatSupported ? C.FORMAT_HANDLED : C.FORMAT_EXCEEDS_CAPABILITIES; + return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java index 5b0a858678..850a9c8db0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.audio; import static com.google.android.exoplayer2.C.FORMAT_HANDLED; import static com.google.android.exoplayer2.RendererCapabilities.ADAPTIVE_NOT_SEAMLESS; -import static com.google.android.exoplayer2.RendererCapabilities.DECODER_SUPPORT_PRIMARY; import static com.google.android.exoplayer2.RendererCapabilities.TUNNELING_NOT_SUPPORTED; import static com.google.android.exoplayer2.RendererCapabilities.TUNNELING_SUPPORTED; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM; @@ -93,11 +92,7 @@ public class DecoderAudioRendererTest { @Test public void supportsFormatAtApi19() { assertThat(audioRenderer.supportsFormat(FORMAT)) - .isEqualTo( - ADAPTIVE_NOT_SEAMLESS - | TUNNELING_NOT_SUPPORTED - | FORMAT_HANDLED - | DECODER_SUPPORT_PRIMARY); + .isEqualTo(ADAPTIVE_NOT_SEAMLESS | TUNNELING_NOT_SUPPORTED | FORMAT_HANDLED); } @Config(sdk = 21) @@ -105,8 +100,7 @@ public class DecoderAudioRendererTest { public void supportsFormatAtApi21() { // From API 21, tunneling is supported. assertThat(audioRenderer.supportsFormat(FORMAT)) - .isEqualTo( - ADAPTIVE_NOT_SEAMLESS | TUNNELING_SUPPORTED | FORMAT_HANDLED | DECODER_SUPPORT_PRIMARY); + .isEqualTo(ADAPTIVE_NOT_SEAMLESS | TUNNELING_SUPPORTED | FORMAT_HANDLED); } @Test From 3a7f7e81d7cba1ab17156d6fa244b272b66db515 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 14 Dec 2021 01:14:15 +0000 Subject: [PATCH 10/56] Rollback of https://github.com/google/ExoPlayer/commit/27f905f571c0594e9140b9a8a4264affb1c000de *** Original commit *** Don't sort decoders by format support in supportsFormat This is a no-op change that updates supportsFormat to use the decoder list before it's reordered by format support. Instead, supportsFormat iterates through the decoders listed in their original priority order as specified by the MediaCodecSelector. The end result is identical. This is necessary groundwork for a subsequent change that will indicate in Capabilities whether the decoder that suppports the format is the primary one as specifi *** PiperOrigin-RevId: 416170612 --- .../audio/MediaCodecAudioRenderer.java | 43 ++----------------- .../video/MediaCodecVideoRenderer.java | 38 ++-------------- 2 files changed, 7 insertions(+), 74 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index fc9a102406..f4c6413c9b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -303,28 +303,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE); } List decoderInfos = - getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false, audioSink); + getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false); if (decoderInfos.isEmpty()) { return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE); } if (!supportsFormatDrm) { return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_DRM); } - // Check whether the first decoder supports the format. This is the preferred decoder for the - // format's MIME type, according to the MediaCodecSelector. + // Check capabilities for the first decoder in the list, which takes priority. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); - if (!isFormatSupported) { - // Check whether any of the other decoders support the format. - for (int i = 1; i < decoderInfos.size(); i++) { - MediaCodecInfo otherDecoderInfo = decoderInfos.get(i); - if (otherDecoderInfo.isFormatSupported(format)) { - decoderInfo = otherDecoderInfo; - isFormatSupported = true; - break; - } - } - } @AdaptiveSupport int adaptiveSupport = isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format) @@ -339,32 +327,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media protected List getDecoderInfos( MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) throws DecoderQueryException { - return MediaCodecUtil.getDecoderInfosSortedByFormatSupport( - getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, audioSink), format); - } - - /** - * Returns a list of decoders that can decode media in the specified format, in the priority order - * specified by the {@link MediaCodecSelector}. Note that since the {@link MediaCodecSelector} - * only has access to {@link Format#sampleMimeType}, the list is not ordered to account for - * whether each decoder supports the details of the format (e.g., taking into account the format's - * profile, level, channel count and so on). {@link - * MediaCodecUtil#getDecoderInfosSortedByFormatSupport} can be used to further sort the list into - * an order where decoders that fully support the format come first. - * - * @param mediaCodecSelector The decoder selector. - * @param format The {@link Format} for which a decoder is required. - * @param requiresSecureDecoder Whether a secure decoder is required. - * @param audioSink The {@link AudioSink} to which audio will be output. - * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty. - * @throws DecoderQueryException Thrown if there was an error querying decoders. - */ - private static List getDecoderInfos( - MediaCodecSelector mediaCodecSelector, - Format format, - boolean requiresSecureDecoder, - AudioSink audioSink) - throws DecoderQueryException { @Nullable String mimeType = format.sampleMimeType; if (mimeType == null) { return Collections.emptyList(); @@ -379,6 +341,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media List decoderInfos = mediaCodecSelector.getDecoderInfos( mimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); + decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D. List decoderInfosWithEac3 = new ArrayList<>(decoderInfos); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index bf4d185f70..4c1d3d6b16 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -370,21 +370,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (!supportsFormatDrm(format)) { return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_DRM); } - // Check whether the first decoder supports the format. This is the preferred decoder for the - // format's MIME type, according to the MediaCodecSelector. + // Check capabilities for the first decoder in the list, which takes priority. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); - if (!isFormatSupported) { - // Check whether any of the other decoders support the format. - for (int i = 1; i < decoderInfos.size(); i++) { - MediaCodecInfo otherDecoderInfo = decoderInfos.get(i); - if (otherDecoderInfo.isFormatSupported(format)) { - decoderInfo = otherDecoderInfo; - isFormatSupported = true; - break; - } - } - } @AdaptiveSupport int adaptiveSupport = decoderInfo.isSeamlessAdaptationSupported(format) @@ -399,9 +387,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { requiresSecureDecryption, /* requiresTunnelingDecoder= */ true); if (!tunnelingDecoderInfos.isEmpty()) { - MediaCodecInfo tunnelingDecoderInfo = - MediaCodecUtil.getDecoderInfosSortedByFormatSupport(tunnelingDecoderInfos, format) - .get(0); + MediaCodecInfo tunnelingDecoderInfo = tunnelingDecoderInfos.get(0); if (tunnelingDecoderInfo.isFormatSupported(format) && tunnelingDecoderInfo.isSeamlessAdaptationSupported(format)) { tunnelingSupport = TUNNELING_SUPPORTED; @@ -417,26 +403,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { protected List getDecoderInfos( MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) throws DecoderQueryException { - return MediaCodecUtil.getDecoderInfosSortedByFormatSupport( - getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, tunneling), format); + return getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, tunneling); } - /** - * Returns a list of decoders that can decode media in the specified format, in the priority order - * specified by the {@link MediaCodecSelector}. Note that since the {@link MediaCodecSelector} - * only has access to {@link Format#sampleMimeType}, the list is not ordered to account for - * whether each decoder supports the details of the format (e.g., taking into account the format's - * profile, level, resolution and so on). {@link - * MediaCodecUtil#getDecoderInfosSortedByFormatSupport} can be used to further sort the list into - * an order where decoders that fully support the format come first. - * - * @param mediaCodecSelector The decoder selector. - * @param format The {@link Format} for which a decoder is required. - * @param requiresSecureDecoder Whether a secure decoder is required. - * @param requiresTunnelingDecoder Whether a tunneling decoder is required. - * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty. - * @throws DecoderQueryException Thrown if there was an error querying decoders. - */ private static List getDecoderInfos( MediaCodecSelector mediaCodecSelector, Format format, @@ -450,6 +419,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { List decoderInfos = mediaCodecSelector.getDecoderInfos( mimeType, requiresSecureDecoder, requiresTunnelingDecoder); + decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); if (MimeTypes.VIDEO_DOLBY_VISION.equals(mimeType)) { // Fall back to H.264/AVC or H.265/HEVC for the relevant DV profiles. This can't be done for // profile CodecProfileLevel.DolbyVisionProfileDvheStn and profile From c5d94c86676a83f91cabf366bc412b403f961af6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Dec 2021 12:39:19 +0000 Subject: [PATCH 11/56] Rollback of https://github.com/google/ExoPlayer/commit/3a7f7e81d7cba1ab17156d6fa244b272b66db515 *** Original commit *** Rollback of https://github.com/google/ExoPlayer/commit/27f905f571c0594e9140b9a8a4264affb1c000de *** Original commit *** Don't sort decoders by format support in supportsFormat This is a no-op change that updates supportsFormat to use the decoder list before it's reordered by format support. Instead, supportsFormat iterates through the decoders listed in their original priority order as specified by the MediaCodecSelector. The end result is identical. This is n... *** PiperOrigin-RevId: 416269130 --- .../audio/MediaCodecAudioRenderer.java | 43 ++++++++++++++- .../video/MediaCodecVideoRenderer.java | 55 ++++++++++++++++--- 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index f4c6413c9b..fc9a102406 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -303,16 +303,28 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE); } List decoderInfos = - getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false); + getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false, audioSink); if (decoderInfos.isEmpty()) { return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE); } if (!supportsFormatDrm) { return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_DRM); } - // Check capabilities for the first decoder in the list, which takes priority. + // Check whether the first decoder supports the format. This is the preferred decoder for the + // format's MIME type, according to the MediaCodecSelector. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); + if (!isFormatSupported) { + // Check whether any of the other decoders support the format. + for (int i = 1; i < decoderInfos.size(); i++) { + MediaCodecInfo otherDecoderInfo = decoderInfos.get(i); + if (otherDecoderInfo.isFormatSupported(format)) { + decoderInfo = otherDecoderInfo; + isFormatSupported = true; + break; + } + } + } @AdaptiveSupport int adaptiveSupport = isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format) @@ -327,6 +339,32 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media protected List getDecoderInfos( MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) throws DecoderQueryException { + return MediaCodecUtil.getDecoderInfosSortedByFormatSupport( + getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, audioSink), format); + } + + /** + * Returns a list of decoders that can decode media in the specified format, in the priority order + * specified by the {@link MediaCodecSelector}. Note that since the {@link MediaCodecSelector} + * only has access to {@link Format#sampleMimeType}, the list is not ordered to account for + * whether each decoder supports the details of the format (e.g., taking into account the format's + * profile, level, channel count and so on). {@link + * MediaCodecUtil#getDecoderInfosSortedByFormatSupport} can be used to further sort the list into + * an order where decoders that fully support the format come first. + * + * @param mediaCodecSelector The decoder selector. + * @param format The {@link Format} for which a decoder is required. + * @param requiresSecureDecoder Whether a secure decoder is required. + * @param audioSink The {@link AudioSink} to which audio will be output. + * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty. + * @throws DecoderQueryException Thrown if there was an error querying decoders. + */ + private static List getDecoderInfos( + MediaCodecSelector mediaCodecSelector, + Format format, + boolean requiresSecureDecoder, + AudioSink audioSink) + throws DecoderQueryException { @Nullable String mimeType = format.sampleMimeType; if (mimeType == null) { return Collections.emptyList(); @@ -341,7 +379,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media List decoderInfos = mediaCodecSelector.getDecoderInfos( mimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); - decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D. List decoderInfosWithEac3 = new ArrayList<>(decoderInfos); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 4c1d3d6b16..ca15b69ce5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -65,6 +65,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; +import com.google.common.collect.ImmutableList; import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; @@ -370,9 +371,21 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (!supportsFormatDrm(format)) { return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_DRM); } - // Check capabilities for the first decoder in the list, which takes priority. + // Check whether the first decoder supports the format. This is the preferred decoder for the + // format's MIME type, according to the MediaCodecSelector. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); + if (!isFormatSupported) { + // Check whether any of the other decoders support the format. + for (int i = 1; i < decoderInfos.size(); i++) { + MediaCodecInfo otherDecoderInfo = decoderInfos.get(i); + if (otherDecoderInfo.isFormatSupported(format)) { + decoderInfo = otherDecoderInfo; + isFormatSupported = true; + break; + } + } + } @AdaptiveSupport int adaptiveSupport = decoderInfo.isSeamlessAdaptationSupported(format) @@ -387,7 +400,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { requiresSecureDecryption, /* requiresTunnelingDecoder= */ true); if (!tunnelingDecoderInfos.isEmpty()) { - MediaCodecInfo tunnelingDecoderInfo = tunnelingDecoderInfos.get(0); + MediaCodecInfo tunnelingDecoderInfo = + MediaCodecUtil.getDecoderInfosSortedByFormatSupport(tunnelingDecoderInfos, format) + .get(0); if (tunnelingDecoderInfo.isFormatSupported(format) && tunnelingDecoderInfo.isSeamlessAdaptationSupported(format)) { tunnelingSupport = TUNNELING_SUPPORTED; @@ -403,9 +418,26 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { protected List getDecoderInfos( MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) throws DecoderQueryException { - return getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, tunneling); + return MediaCodecUtil.getDecoderInfosSortedByFormatSupport( + getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, tunneling), format); } + /** + * Returns a list of decoders that can decode media in the specified format, in the priority order + * specified by the {@link MediaCodecSelector}. Note that since the {@link MediaCodecSelector} + * only has access to {@link Format#sampleMimeType}, the list is not ordered to account for + * whether each decoder supports the details of the format (e.g., taking into account the format's + * profile, level, resolution and so on). {@link + * MediaCodecUtil#getDecoderInfosSortedByFormatSupport} can be used to further sort the list into + * an order where decoders that fully support the format come first. + * + * @param mediaCodecSelector The decoder selector. + * @param format The {@link Format} for which a decoder is required. + * @param requiresSecureDecoder Whether a secure decoder is required. + * @param requiresTunnelingDecoder Whether a tunneling decoder is required. + * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty. + * @throws DecoderQueryException Thrown if there was an error querying decoders. + */ private static List getDecoderInfos( MediaCodecSelector mediaCodecSelector, Format format, @@ -419,7 +451,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { List decoderInfos = mediaCodecSelector.getDecoderInfos( mimeType, requiresSecureDecoder, requiresTunnelingDecoder); - decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); if (MimeTypes.VIDEO_DOLBY_VISION.equals(mimeType)) { // Fall back to H.264/AVC or H.265/HEVC for the relevant DV profiles. This can't be done for // profile CodecProfileLevel.DolbyVisionProfileDvheStn and profile @@ -428,17 +459,25 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Nullable Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); if (codecProfileAndLevel != null) { + List fallbackDecoderInfos; int profile = codecProfileAndLevel.first; if (profile == CodecProfileLevel.DolbyVisionProfileDvheDtr || profile == CodecProfileLevel.DolbyVisionProfileDvheSt) { - decoderInfos.addAll( + fallbackDecoderInfos = mediaCodecSelector.getDecoderInfos( - MimeTypes.VIDEO_H265, requiresSecureDecoder, requiresTunnelingDecoder)); + MimeTypes.VIDEO_H265, requiresSecureDecoder, requiresTunnelingDecoder); } else if (profile == CodecProfileLevel.DolbyVisionProfileDvavSe) { - decoderInfos.addAll( + fallbackDecoderInfos = mediaCodecSelector.getDecoderInfos( - MimeTypes.VIDEO_H264, requiresSecureDecoder, requiresTunnelingDecoder)); + MimeTypes.VIDEO_H264, requiresSecureDecoder, requiresTunnelingDecoder); + } else { + fallbackDecoderInfos = ImmutableList.of(); } + decoderInfos = + ImmutableList.builder() + .addAll(decoderInfos) + .addAll(fallbackDecoderInfos) + .build(); } } return Collections.unmodifiableList(decoderInfos); From 27b52bca4115db7ca8613f8499fd1a3abfdd6667 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Dec 2021 14:18:13 +0000 Subject: [PATCH 12/56] Rollback of https://github.com/google/ExoPlayer/commit/bf1224186c5bb9751dbb353cbd9e878129b2c2b0 *** Original commit *** Rollback of https://github.com/google/ExoPlayer/commit/0aa23b08b1624cacc9414cd093ff64ca1e0b1a10 *** Original commit *** Add capability flags for hardware and decoder support Issue: google/ExoPlayer#9565 *** *** PiperOrigin-RevId: 416285603 --- .../exoplayer2/RendererCapabilities.java | 141 ++++++++++++++++-- .../audio/MediaCodecAudioRenderer.java | 20 ++- .../trackselection/MappingTrackSelector.java | 15 +- .../android/exoplayer2/util/EventLogger.java | 26 +++- .../video/MediaCodecVideoRenderer.java | 22 ++- .../audio/DecoderAudioRendererTest.java | 10 +- 6 files changed, 204 insertions(+), 30 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index 62eaa49b9e..654ea54409 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -60,16 +60,16 @@ public interface RendererCapabilities { @interface AdaptiveSupport {} /** A mask to apply to {@link Capabilities} to obtain the {@link AdaptiveSupport} only. */ - int ADAPTIVE_SUPPORT_MASK = 0b11000; + int ADAPTIVE_SUPPORT_MASK = 0b11 << 3; /** The {@link Renderer} can seamlessly adapt between formats. */ - int ADAPTIVE_SEAMLESS = 0b10000; + int ADAPTIVE_SEAMLESS = 0b10 << 3; /** * The {@link Renderer} can adapt between formats, but may suffer a brief discontinuity * (~50-100ms) when adaptation occurs. */ - int ADAPTIVE_NOT_SEAMLESS = 0b01000; + int ADAPTIVE_NOT_SEAMLESS = 0b01 << 3; /** The {@link Renderer} does not support adaptation between formats. */ - int ADAPTIVE_NOT_SUPPORTED = 0b00000; + int ADAPTIVE_NOT_SUPPORTED = 0; /** * Level of renderer support for tunneling. One of {@link #TUNNELING_SUPPORTED} or {@link @@ -80,20 +80,62 @@ public interface RendererCapabilities { @IntDef({TUNNELING_SUPPORTED, TUNNELING_NOT_SUPPORTED}) @interface TunnelingSupport {} - /** A mask to apply to {@link Capabilities} to obtain the {@link TunnelingSupport} only. */ - int TUNNELING_SUPPORT_MASK = 0b100000; + /** A mask to apply to {@link Capabilities} to obtain {@link TunnelingSupport} only. */ + int TUNNELING_SUPPORT_MASK = 0b1 << 5; /** The {@link Renderer} supports tunneled output. */ - int TUNNELING_SUPPORTED = 0b100000; + int TUNNELING_SUPPORTED = 0b1 << 5; /** The {@link Renderer} does not support tunneled output. */ - int TUNNELING_NOT_SUPPORTED = 0b000000; + int TUNNELING_NOT_SUPPORTED = 0; + + /** + * Level of renderer support for hardware acceleration. One of {@link + * #HARDWARE_ACCELERATION_SUPPORTED} and {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED}. + * + *

    For video renderers, the level of support is indicated for non-tunneled output. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + HARDWARE_ACCELERATION_SUPPORTED, + HARDWARE_ACCELERATION_NOT_SUPPORTED, + }) + @interface HardwareAccelerationSupport {} + /** A mask to apply to {@link Capabilities} to obtain {@link HardwareAccelerationSupport} only. */ + int HARDWARE_ACCELERATION_SUPPORT_MASK = 0b1 << 6; + /** The renderer is able to use hardware acceleration. */ + int HARDWARE_ACCELERATION_SUPPORTED = 0b1 << 6; + /** The renderer is not able to use hardware acceleration. */ + int HARDWARE_ACCELERATION_NOT_SUPPORTED = 0; + + /** + * Level of decoder support. One of {@link #DECODER_SUPPORT_PRIMARY} and {@link + * #DECODER_SUPPORT_FALLBACK}. + * + *

    For video renderers, the level of support is indicated for non-tunneled output. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DECODER_SUPPORT_PRIMARY, + DECODER_SUPPORT_FALLBACK, + }) + @interface DecoderSupport {} + /** A mask to apply to {@link Capabilities} to obtain {@link DecoderSupport} only. */ + int MODE_SUPPORT_MASK = 0b1 << 7; + /** The renderer is able to use the primary decoder for the format's MIME type. */ + int DECODER_SUPPORT_PRIMARY = 0b1 << 7; + /** The renderer will use a fallback decoder. */ + int DECODER_SUPPORT_FALLBACK = 0; /** * Combined renderer capabilities. * - *

    This is a bitwise OR of {@link C.FormatSupport}, {@link AdaptiveSupport} and {@link - * TunnelingSupport}. Use {@link #getFormatSupport(int)}, {@link #getAdaptiveSupport(int)} or - * {@link #getTunnelingSupport(int)} to obtain the individual flags. And use {@link #create(int)} - * or {@link #create(int, int, int)} to create the combined capabilities. + *

    This is a bitwise OR of {@link C.FormatSupport}, {@link AdaptiveSupport}, {@link + * TunnelingSupport}, {@link HardwareAccelerationSupport} and {@link DecoderSupport}. Use {@link + * #getFormatSupport}, {@link #getAdaptiveSupport}, {@link #getTunnelingSupport}, {@link + * #getHardwareAccelerationSupport} and {@link #getDecoderSupport} to obtain individual + * components. Use {@link #create(int)}, {@link #create(int, int, int)} or {@link #create(int, + * int, int, int, int)} to create combined capabilities from individual components. * *

    Possible values: * @@ -111,6 +153,11 @@ public interface RendererCapabilities { * #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of * support for the format itself is {@link C#FORMAT_HANDLED} or {@link * C#FORMAT_EXCEEDS_CAPABILITIES}. + *

  • {@link HardwareAccelerationSupport}: The level of support for hardware acceleration. One + * of {@link #HARDWARE_ACCELERATION_SUPPORTED} and {@link + * #HARDWARE_ACCELERATION_NOT_SUPPORTED}. + *
  • {@link DecoderSupport}: The level of decoder support. One of {@link + * #DECODER_SUPPORT_PRIMARY} and {@link #DECODER_SUPPORT_FALLBACK}. * */ @Documented @@ -122,8 +169,10 @@ public interface RendererCapabilities { /** * Returns {@link Capabilities} for the given {@link C.FormatSupport}. * - *

    The {@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED} and {{@link - * TunnelingSupport} is set to {@link #TUNNELING_NOT_SUPPORTED}. + *

    {@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED}, {@link TunnelingSupport} + * is set to {@link #TUNNELING_NOT_SUPPORTED}, {@link HardwareAccelerationSupport} is set to + * {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED} and {@link DecoderSupport} is set to {@link + * #DECODER_SUPPORT_PRIMARY}. * * @param formatSupport The {@link C.FormatSupport}. * @return The combined {@link Capabilities} of the given {@link C.FormatSupport}, {@link @@ -138,19 +187,53 @@ public interface RendererCapabilities { * Returns {@link Capabilities} combining the given {@link C.FormatSupport}, {@link * AdaptiveSupport} and {@link TunnelingSupport}. * + *

    {@link HardwareAccelerationSupport} is set to {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED} + * and {@link DecoderSupport} is set to {@link #DECODER_SUPPORT_PRIMARY}. + * * @param formatSupport The {@link C.FormatSupport}. * @param adaptiveSupport The {@link AdaptiveSupport}. * @param tunnelingSupport The {@link TunnelingSupport}. * @return The combined {@link Capabilities}. */ + @Capabilities + static int create( + @C.FormatSupport int formatSupport, + @AdaptiveSupport int adaptiveSupport, + @TunnelingSupport int tunnelingSupport) { + return create( + formatSupport, + adaptiveSupport, + tunnelingSupport, + HARDWARE_ACCELERATION_NOT_SUPPORTED, + DECODER_SUPPORT_PRIMARY); + } + + /** + * Returns {@link Capabilities} combining the given {@link C.FormatSupport}, {@link + * AdaptiveSupport}, {@link TunnelingSupport}, {@link HardwareAccelerationSupport} and {@link + * DecoderSupport}. + * + * @param formatSupport The {@link C.FormatSupport}. + * @param adaptiveSupport The {@link AdaptiveSupport}. + * @param tunnelingSupport The {@link TunnelingSupport}. + * @param hardwareAccelerationSupport The {@link HardwareAccelerationSupport}. + * @param decoderSupport The {@link DecoderSupport}. + * @return The combined {@link Capabilities}. + */ // Suppression needed for IntDef casting. @SuppressLint("WrongConstant") @Capabilities static int create( @C.FormatSupport int formatSupport, @AdaptiveSupport int adaptiveSupport, - @TunnelingSupport int tunnelingSupport) { - return formatSupport | adaptiveSupport | tunnelingSupport; + @TunnelingSupport int tunnelingSupport, + @HardwareAccelerationSupport int hardwareAccelerationSupport, + @DecoderSupport int decoderSupport) { + return formatSupport + | adaptiveSupport + | tunnelingSupport + | hardwareAccelerationSupport + | decoderSupport; } /** @@ -192,6 +275,32 @@ public interface RendererCapabilities { return supportFlags & TUNNELING_SUPPORT_MASK; } + /** + * Returns the {@link HardwareAccelerationSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link HardwareAccelerationSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @HardwareAccelerationSupport + static int getHardwareAccelerationSupport(@Capabilities int supportFlags) { + return supportFlags & HARDWARE_ACCELERATION_SUPPORT_MASK; + } + + /** + * Returns the {@link DecoderSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link DecoderSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @DecoderSupport + static int getDecoderSupport(@Capabilities int supportFlags) { + return supportFlags & MODE_SUPPORT_MASK; + } + /** Returns the name of the {@link Renderer}. */ String getName(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index fc9a102406..41234fa828 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -314,6 +314,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media // format's MIME type, according to the MediaCodecSelector. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); + boolean isPreferredDecoder = true; if (!isFormatSupported) { // Check whether any of the other decoders support the format. for (int i = 1; i < decoderInfos.size(); i++) { @@ -321,18 +322,31 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media if (otherDecoderInfo.isFormatSupported(format)) { decoderInfo = otherDecoderInfo; isFormatSupported = true; + isPreferredDecoder = false; break; } } } + @C.FormatSupport + int formatSupport = isFormatSupported ? C.FORMAT_HANDLED : C.FORMAT_EXCEEDS_CAPABILITIES; @AdaptiveSupport int adaptiveSupport = isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format) ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; - @C.FormatSupport - int formatSupport = isFormatSupported ? C.FORMAT_HANDLED : C.FORMAT_EXCEEDS_CAPABILITIES; - return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); + @HardwareAccelerationSupport + int hardwareAccelerationSupport = + decoderInfo.hardwareAccelerated + ? HARDWARE_ACCELERATION_SUPPORTED + : HARDWARE_ACCELERATION_NOT_SUPPORTED; + @DecoderSupport + int decoderSupport = isPreferredDecoder ? DECODER_SUPPORT_PRIMARY : DECODER_SUPPORT_FALLBACK; + return RendererCapabilities.create( + formatSupport, + adaptiveSupport, + tunnelingSupport, + hardwareAccelerationSupport, + decoderSupport); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 226e7b24ee..37040c944f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -216,6 +216,19 @@ public abstract class MappingTrackSelector extends TrackSelector { return bestRendererSupport; } + /** + * Returns the {@link Capabilities} of the renderer for an individual track. + * + * @param rendererIndex The renderer index. + * @param groupIndex The index of the track group to which the track belongs. + * @param trackIndex The index of the track within the track group. + * @return The {@link Capabilities}. + */ + @Capabilities + public int getCapabilities(int rendererIndex, int groupIndex, int trackIndex) { + return rendererFormatSupports[rendererIndex][groupIndex][trackIndex]; + } + /** * Returns the extent to which an individual track is supported by the renderer. * @@ -227,7 +240,7 @@ public abstract class MappingTrackSelector extends TrackSelector { @FormatSupport public int getTrackSupport(int rendererIndex, int groupIndex, int trackIndex) { return RendererCapabilities.getFormatSupport( - rendererFormatSupports[rendererIndex][groupIndex][trackIndex]); + getCapabilities(rendererIndex, groupIndex, trackIndex)); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index 565f3eb880..00eb88cb80 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -15,6 +15,12 @@ */ package com.google.android.exoplayer2.util; +import static com.google.android.exoplayer2.RendererCapabilities.DECODER_SUPPORT_FALLBACK; +import static com.google.android.exoplayer2.RendererCapabilities.HARDWARE_ACCELERATION_SUPPORTED; +import static com.google.android.exoplayer2.RendererCapabilities.getDecoderSupport; +import static com.google.android.exoplayer2.RendererCapabilities.getFormatSupport; +import static com.google.android.exoplayer2.RendererCapabilities.getHardwareAccelerationSupport; +import static com.google.android.exoplayer2.util.Util.getFormatSupportString; import static java.lang.Math.min; import android.os.SystemClock; @@ -29,6 +35,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -276,9 +283,16 @@ public class EventLogger implements AnalyticsListener { logd(" Group:" + trackGroup.id + ", adaptive_supported=" + adaptiveSupport + " ["); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); - String formatSupport = - Util.getFormatSupportString( - mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)); + @Capabilities + int capabilities = + mappedTrackInfo.getCapabilities(rendererIndex, groupIndex, trackIndex); + String formatSupport = getFormatSupportString(getFormatSupport(capabilities)); + String hardwareAccelerationSupport = + getHardwareAccelerationSupport(capabilities) == HARDWARE_ACCELERATION_SUPPORTED + ? ", accelerated=YES" + : ""; + String decoderSupport = + getDecoderSupport(capabilities) == DECODER_SUPPORT_FALLBACK ? ", fallback=YES" : ""; logd( " " + status @@ -287,7 +301,9 @@ public class EventLogger implements AnalyticsListener { + ", " + Format.toLogString(trackGroup.getFormat(trackIndex)) + ", supported=" - + formatSupport); + + formatSupport + + hardwareAccelerationSupport + + decoderSupport); } logd(" ]"); } @@ -315,7 +331,7 @@ public class EventLogger implements AnalyticsListener { TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(false); - String formatSupport = Util.getFormatSupportString(C.FORMAT_UNSUPPORTED_TYPE); + String formatSupport = getFormatSupportString(C.FORMAT_UNSUPPORTED_TYPE); logd( " " + status diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index ca15b69ce5..c25d915b26 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -375,6 +375,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // format's MIME type, according to the MediaCodecSelector. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); + boolean isPreferredDecoder = true; if (!isFormatSupported) { // Check whether any of the other decoders support the format. for (int i = 1; i < decoderInfos.size(); i++) { @@ -382,15 +383,26 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (otherDecoderInfo.isFormatSupported(format)) { decoderInfo = otherDecoderInfo; isFormatSupported = true; + isPreferredDecoder = false; break; } } } + @C.FormatSupport + int formatSupport = isFormatSupported ? C.FORMAT_HANDLED : C.FORMAT_EXCEEDS_CAPABILITIES; @AdaptiveSupport int adaptiveSupport = decoderInfo.isSeamlessAdaptationSupported(format) ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; + @HardwareAccelerationSupport + int hardwareAccelerationSupport = + decoderInfo.hardwareAccelerated + ? HARDWARE_ACCELERATION_SUPPORTED + : HARDWARE_ACCELERATION_NOT_SUPPORTED; + @DecoderSupport + int decoderSupport = isPreferredDecoder ? DECODER_SUPPORT_PRIMARY : DECODER_SUPPORT_FALLBACK; + @TunnelingSupport int tunnelingSupport = TUNNELING_NOT_SUPPORTED; if (isFormatSupported) { List tunnelingDecoderInfos = @@ -409,9 +421,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } } - @C.FormatSupport - int formatSupport = isFormatSupported ? C.FORMAT_HANDLED : C.FORMAT_EXCEEDS_CAPABILITIES; - return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); + + return RendererCapabilities.create( + formatSupport, + adaptiveSupport, + tunnelingSupport, + hardwareAccelerationSupport, + decoderSupport); } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java index 850a9c8db0..5b0a858678 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio; import static com.google.android.exoplayer2.C.FORMAT_HANDLED; import static com.google.android.exoplayer2.RendererCapabilities.ADAPTIVE_NOT_SEAMLESS; +import static com.google.android.exoplayer2.RendererCapabilities.DECODER_SUPPORT_PRIMARY; import static com.google.android.exoplayer2.RendererCapabilities.TUNNELING_NOT_SUPPORTED; import static com.google.android.exoplayer2.RendererCapabilities.TUNNELING_SUPPORTED; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM; @@ -92,7 +93,11 @@ public class DecoderAudioRendererTest { @Test public void supportsFormatAtApi19() { assertThat(audioRenderer.supportsFormat(FORMAT)) - .isEqualTo(ADAPTIVE_NOT_SEAMLESS | TUNNELING_NOT_SUPPORTED | FORMAT_HANDLED); + .isEqualTo( + ADAPTIVE_NOT_SEAMLESS + | TUNNELING_NOT_SUPPORTED + | FORMAT_HANDLED + | DECODER_SUPPORT_PRIMARY); } @Config(sdk = 21) @@ -100,7 +105,8 @@ public class DecoderAudioRendererTest { public void supportsFormatAtApi21() { // From API 21, tunneling is supported. assertThat(audioRenderer.supportsFormat(FORMAT)) - .isEqualTo(ADAPTIVE_NOT_SEAMLESS | TUNNELING_SUPPORTED | FORMAT_HANDLED); + .isEqualTo( + ADAPTIVE_NOT_SEAMLESS | TUNNELING_SUPPORTED | FORMAT_HANDLED | DECODER_SUPPORT_PRIMARY); } @Test From 5bd22c3ab71d0baa710a08f6aff71c863a9db02b Mon Sep 17 00:00:00 2001 From: hschlueter Date: Tue, 14 Dec 2021 16:11:55 +0000 Subject: [PATCH 13/56] Use TransformationException for error listener parameter. PiperOrigin-RevId: 416307600 --- RELEASENOTES.md | 2 ++ docs/transforming-media.md | 2 +- .../transformer/mh/AndroidTestUtil.java | 4 +++- .../exoplayer2/transformer/Transformer.java | 17 ++++++++++++----- .../exoplayer2/transformer/TransformerTest.java | 6 ++---- .../transformer/TransformerTestRunner.java | 13 ++++++++----- 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 50517a1440..e78187d458 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -67,6 +67,8 @@ ([#9765](https://github.com/google/ExoPlayer/issues/9765)). * Transformer: * Increase required min API version to 21. + * `TransformationException` is now used to describe errors that occur + during a transformation. * MediaSession extension: * Remove deprecated call to `onStop(/* reset= */ true)` and provide an opt-out flag for apps that don't want to clear the playlist on stop. diff --git a/docs/transforming-media.md b/docs/transforming-media.md index 94f2a17037..db0e893839 100644 --- a/docs/transforming-media.md +++ b/docs/transforming-media.md @@ -70,7 +70,7 @@ Transformer.Listener transformerListener = } @Override - public void onTransformationError(MediaItem inputMediaItem, Exception e) { + public void onTransformationError(MediaItem inputMediaItem, TransformationException e) { displayError(e); } }; diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/AndroidTestUtil.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/AndroidTestUtil.java index 6634d4fd8f..6f54d93949 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/AndroidTestUtil.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/AndroidTestUtil.java @@ -25,6 +25,7 @@ import android.os.Build; import androidx.annotation.Nullable; import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.transformer.TransformationException; import com.google.android.exoplayer2.transformer.Transformer; import java.io.File; import java.io.FileWriter; @@ -79,7 +80,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } @Override - public void onTransformationError(MediaItem inputMediaItem, Exception exception) { + public void onTransformationError( + MediaItem inputMediaItem, TransformationException exception) { exceptionReference.set(exception); countDownLatch.countDown(); } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java index ab52a5cf40..3ceb6710e8 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java @@ -466,19 +466,26 @@ public final class Transformer { public interface Listener { /** - * Called when the transformation is completed. + * Called when the transformation is completed successfully. * * @param inputMediaItem The {@link MediaItem} for which the transformation is completed. */ default void onTransformationCompleted(MediaItem inputMediaItem) {} + /** @deprecated Use {@link #onTransformationError(MediaItem, TransformationException)}. */ + @Deprecated + default void onTransformationError(MediaItem inputMediaItem, Exception exception) { + onTransformationError(inputMediaItem, (TransformationException) exception); + } + /** - * Called if an error occurs during the transformation. + * Called if an exception occurs during the transformation. * - * @param inputMediaItem The {@link MediaItem} for which the error occurs. - * @param exception The exception describing the error. + * @param inputMediaItem The {@link MediaItem} for which the exception occurs. + * @param exception The {@link TransformationException} describing the exception. */ - default void onTransformationError(MediaItem inputMediaItem, Exception exception) {} + default void onTransformationError( + MediaItem inputMediaItem, TransformationException exception) {} } /** Provider for views to show diagnostic information during transformation, for debugging. */ diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java index e477c2140c..805b2ae892 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java @@ -238,9 +238,8 @@ public final class TransformerTest { MediaItem mediaItem = MediaItem.fromUri("asset:///non-existing-path.mp4"); transformer.startTransformation(mediaItem, outputPath); - Exception exception = TransformerTestRunner.runUntilError(transformer); + TransformationException exception = TransformerTestRunner.runUntilError(transformer); - assertThat(exception).isInstanceOf(TransformationException.class); assertThat(exception).hasCauseThat().isInstanceOf(ExoPlaybackException.class); assertThat(exception).hasCauseThat().hasCauseThat().isInstanceOf(IOException.class); } @@ -252,9 +251,8 @@ public final class TransformerTest { MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_WITH_ALL_SAMPLE_FORMATS_UNSUPPORTED); transformer.startTransformation(mediaItem, outputPath); - Exception exception = TransformerTestRunner.runUntilError(transformer); + TransformationException exception = TransformerTestRunner.runUntilError(transformer); - assertThat(exception).isInstanceOf(TransformationException.class); assertThat(exception).hasCauseThat().isInstanceOf(IllegalStateException.class); } diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTestRunner.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTestRunner.java index 1eacbc46e0..ba3063f175 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTestRunner.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTestRunner.java @@ -56,8 +56,9 @@ public final class TransformerTestRunner { * @throws IllegalStateException If the method is not called from the main thread, or if the * transformation completes without error. */ - public static Exception runUntilError(Transformer transformer) throws TimeoutException { - @Nullable Exception exception = runUntilListenerCalled(transformer); + public static TransformationException runUntilError(Transformer transformer) + throws TimeoutException { + @Nullable TransformationException exception = runUntilListenerCalled(transformer); if (exception == null) { throw new IllegalStateException("The transformation completed without error."); } @@ -65,7 +66,8 @@ public final class TransformerTestRunner { } @Nullable - private static Exception runUntilListenerCalled(Transformer transformer) throws TimeoutException { + private static TransformationException runUntilListenerCalled(Transformer transformer) + throws TimeoutException { TransformationResult transformationResult = new TransformationResult(); Transformer.Listener listener = new Transformer.Listener() { @@ -75,7 +77,8 @@ public final class TransformerTestRunner { } @Override - public void onTransformationError(MediaItem inputMediaItem, Exception exception) { + public void onTransformationError( + MediaItem inputMediaItem, TransformationException exception) { transformationResult.exception = exception; } }; @@ -88,6 +91,6 @@ public final class TransformerTestRunner { private static class TransformationResult { public boolean isCompleted; - @Nullable public Exception exception; + @Nullable public TransformationException exception; } } From f038258522c6c6a0e5f821509d92bf0c31ae4e0a Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 14 Dec 2021 16:46:57 +0000 Subject: [PATCH 14/56] Add AdPlaybackStateUpdater PiperOrigin-RevId: 416314200 --- .../ads/ServerSideAdInsertionMediaSource.java | 35 ++++++++++++++++--- .../ServerSideAdInsertionMediaSourceTest.java | 19 +++++----- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSource.java index b040966714..5870fb1872 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSource.java @@ -79,10 +79,32 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public final class ServerSideAdInsertionMediaSource extends BaseMediaSource implements MediaSource.MediaSourceCaller, MediaSourceEventListener, DrmSessionEventListener { + /** + * Receives ad playback state update requests when the {@link Timeline} of the content media + * source has changed. + */ + public interface AdPlaybackStateUpdater { + /** + * Called when the content source has refreshed the timeline. + * + *

    If true is returned the source refresh publication is deferred, to wait for an {@link + * #setAdPlaybackState(AdPlaybackState) ad playback state update}. If false is returned, the + * source refresh is immediately published. + * + *

    Called on the playback thread. + * + * @param contentTimeline The {@link Timeline} of the wrapped content media source. + * @return true to defer the source refresh publication, or false to immediately publish the + * source refresh. + */ + boolean onAdPlaybackStateUpdateRequested(Timeline contentTimeline); + } + private final MediaSource mediaSource; private final ListMultimap mediaPeriods; private final MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcherWithoutId; private final DrmSessionEventListener.EventDispatcher drmEventDispatcherWithoutId; + @Nullable private final AdPlaybackStateUpdater adPlaybackStateUpdater; @GuardedBy("this") @Nullable @@ -96,11 +118,15 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource * Creates the media source. * * @param mediaSource The {@link MediaSource} to wrap. + * @param adPlaybackStateUpdater The optional {@link AdPlaybackStateUpdater} to be called before a + * source refresh is published. */ // Calling BaseMediaSource.createEventDispatcher from the constructor. @SuppressWarnings("nullness:method.invocation") - public ServerSideAdInsertionMediaSource(MediaSource mediaSource) { + public ServerSideAdInsertionMediaSource( + MediaSource mediaSource, @Nullable AdPlaybackStateUpdater adPlaybackStateUpdater) { this.mediaSource = mediaSource; + this.adPlaybackStateUpdater = adPlaybackStateUpdater; mediaPeriods = ArrayListMultimap.create(); adPlaybackState = AdPlaybackState.NONE; mediaSourceEventDispatcherWithoutId = createEventDispatcher(/* mediaPeriodId= */ null); @@ -190,10 +216,11 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { this.contentTimeline = timeline; - if (AdPlaybackState.NONE.equals(adPlaybackState)) { - return; + if ((adPlaybackStateUpdater == null + || !adPlaybackStateUpdater.onAdPlaybackStateUpdateRequested(timeline)) + && !AdPlaybackState.NONE.equals(adPlaybackState)) { + refreshSourceInfo(new ServerSideAdInsertionTimeline(timeline, adPlaybackState)); } - refreshSourceInfo(new ServerSideAdInsertionTimeline(timeline, adPlaybackState)); } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSourceTest.java index fcb8d6d08c..8d662bb445 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSourceTest.java @@ -81,7 +81,8 @@ public final class ServerSideAdInsertionMediaSourceTest { /* windowOffsetInFirstPeriodUs= */ 42_000_000L, AdPlaybackState.NONE)); ServerSideAdInsertionMediaSource mediaSource = - new ServerSideAdInsertionMediaSource(new FakeMediaSource(wrappedTimeline)); + new ServerSideAdInsertionMediaSource( + new FakeMediaSource(wrappedTimeline), /* adPlaybackStateUpdater= */ null); // Test with one ad group before the window, and the window starting within the second ad group. AdPlaybackState adPlaybackState = new AdPlaybackState( @@ -154,8 +155,8 @@ public final class ServerSideAdInsertionMediaSourceTest { ServerSideAdInsertionMediaSource mediaSource = new ServerSideAdInsertionMediaSource( - new DefaultMediaSourceFactory(context) - .createMediaSource(MediaItem.fromUri(TEST_ASSET))); + new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), + /* adPlaybackStateUpdater= */ null); AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object()); adPlaybackState = addAdGroupToAdPlaybackState( @@ -213,8 +214,8 @@ public final class ServerSideAdInsertionMediaSourceTest { ServerSideAdInsertionMediaSource mediaSource = new ServerSideAdInsertionMediaSource( - new DefaultMediaSourceFactory(context) - .createMediaSource(MediaItem.fromUri(TEST_ASSET))); + new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), + /* adPlaybackStateUpdater= */ null); AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object()); adPlaybackState = addAdGroupToAdPlaybackState( @@ -273,8 +274,8 @@ public final class ServerSideAdInsertionMediaSourceTest { ServerSideAdInsertionMediaSource mediaSource = new ServerSideAdInsertionMediaSource( - new DefaultMediaSourceFactory(context) - .createMediaSource(MediaItem.fromUri(TEST_ASSET))); + new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), + /* adPlaybackStateUpdater= */ null); AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object()); adPlaybackState = addAdGroupToAdPlaybackState( @@ -327,8 +328,8 @@ public final class ServerSideAdInsertionMediaSourceTest { ServerSideAdInsertionMediaSource mediaSource = new ServerSideAdInsertionMediaSource( - new DefaultMediaSourceFactory(context) - .createMediaSource(MediaItem.fromUri(TEST_ASSET))); + new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), + /* adPlaybackStateUpdater= */ null); AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object()); adPlaybackState = addAdGroupToAdPlaybackState( From a6c26d04fa20924b6a6fb9b4ffa5c1c5d3834edf Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Dec 2021 17:36:52 +0000 Subject: [PATCH 15/56] Remove condition that is always false. The same condition is checked further up on L497 already. PiperOrigin-RevId: 416324687 --- .../android/exoplayer2/audio/MediaCodecAudioRenderer.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 41234fa828..4a2b5314d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -514,10 +514,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } else { // If the format is anything other than PCM then we assume that the audio decoder will // output 16-bit PCM. - pcmEncoding = - MimeTypes.AUDIO_RAW.equals(format.sampleMimeType) - ? format.pcmEncoding - : C.ENCODING_PCM_16BIT; + pcmEncoding = C.ENCODING_PCM_16BIT; } audioSinkInputFormat = new Format.Builder() From 586dd355d1e9d5c602784538287e76f99c78669f Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 14 Dec 2021 17:44:37 +0000 Subject: [PATCH 16/56] Move Player(Control)View resource IDs into separate _legacy.xml files This is a no-op because all the elements from these XML files are effectively concatenated together during building. PiperOrigin-RevId: 416326534 --- library/ui/src/main/res/values/attrs.xml | 65 +-------------- .../ui/src/main/res/values/attrs_legacy.xml | 81 +++++++++++++++++++ library/ui/src/main/res/values/drawables.xml | 14 ---- .../src/main/res/values/drawables_legacy.xml | 31 +++++++ library/ui/src/main/res/values/styles.xml | 41 ---------- .../ui/src/main/res/values/styles_legacy.xml | 58 +++++++++++++ 6 files changed, 171 insertions(+), 119 deletions(-) create mode 100644 library/ui/src/main/res/values/attrs_legacy.xml create mode 100644 library/ui/src/main/res/values/drawables_legacy.xml create mode 100644 library/ui/src/main/res/values/styles_legacy.xml diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 6121aa1402..a0547b53fb 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -24,7 +24,7 @@ - + @@ -91,42 +91,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -171,33 +135,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/library/ui/src/main/res/values/attrs_legacy.xml b/library/ui/src/main/res/values/attrs_legacy.xml new file mode 100644 index 0000000000..1a2b462dee --- /dev/null +++ b/library/ui/src/main/res/values/attrs_legacy.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/ui/src/main/res/values/drawables.xml b/library/ui/src/main/res/values/drawables.xml index f72c57f8a9..9f3227b29e 100644 --- a/library/ui/src/main/res/values/drawables.xml +++ b/library/ui/src/main/res/values/drawables.xml @@ -14,20 +14,6 @@ limitations under the License. --> - @drawable/exo_icon_play - @drawable/exo_icon_pause - @drawable/exo_icon_next - @drawable/exo_icon_previous - @drawable/exo_icon_fastforward - @drawable/exo_icon_rewind - @drawable/exo_icon_repeat_all - @drawable/exo_icon_repeat_off - @drawable/exo_icon_repeat_one - @drawable/exo_icon_shuffle_off - @drawable/exo_icon_shuffle_on - @drawable/exo_icon_fullscreen_enter - @drawable/exo_icon_fullscreen_exit - @drawable/exo_icon_vr @drawable/exo_icon_play @drawable/exo_icon_pause @drawable/exo_icon_next diff --git a/library/ui/src/main/res/values/drawables_legacy.xml b/library/ui/src/main/res/values/drawables_legacy.xml new file mode 100644 index 0000000000..8f438ffb58 --- /dev/null +++ b/library/ui/src/main/res/values/drawables_legacy.xml @@ -0,0 +1,31 @@ + + + + @drawable/exo_icon_play + @drawable/exo_icon_pause + @drawable/exo_icon_next + @drawable/exo_icon_previous + @drawable/exo_icon_fastforward + @drawable/exo_icon_rewind + @drawable/exo_icon_repeat_all + @drawable/exo_icon_repeat_off + @drawable/exo_icon_repeat_one + @drawable/exo_icon_shuffle_off + @drawable/exo_icon_shuffle_on + @drawable/exo_icon_fullscreen_enter + @drawable/exo_icon_fullscreen_exit + @drawable/exo_icon_vr + diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index 66efb3eee6..2e5e080caa 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -15,47 +15,6 @@ --> - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + From b5206b8e050a2570a9b69f9c3108286b85ef0a2e Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 14 Dec 2021 18:51:17 +0000 Subject: [PATCH 17/56] Add getMetrics() in MediaCodecAdapter Before the introduction of the MediaCodecAdapter, users could get access directly to the MediaCodec instance from MediaCodecRenderer.getCodec() and then retrieve the codec metrics. This change exposes MediaCodec.getMetrics() on the MediaCodecAdapter. Issue: google/ExoPlayer#9766 #minor-release PiperOrigin-RevId: 416343023 --- RELEASENOTES.md | 3 +++ .../mediacodec/AsynchronousMediaCodecAdapter.java | 8 ++++++++ .../android/exoplayer2/mediacodec/MediaCodecAdapter.java | 9 +++++++++ .../mediacodec/SynchronousMediaCodecAdapter.java | 9 +++++++++ .../exoplayer2/testutil/CapturingRenderersFactory.java | 7 +++++++ 5 files changed, 36 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e78187d458..5f03454544 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -18,6 +18,9 @@ around an issue that occurs on some devices when switching a surface from a secure codec to another codec (#8696)[https://github.com/google/ExoPlayer/issues/8696]. + * Add `MediaCodecAdapter.getMetrics()` to allow users obtain metrics data + from `MediaCodec`. + ([#9766](https://github.com/google/ExoPlayer/issues/9766)). * Android 12 compatibility: * Upgrade the Cast extension to depend on `com.google.android.gms:play-services-cast-framework:20.1.0`. Earlier diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java index c56bf54f8c..c49bdb4035 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java @@ -22,6 +22,7 @@ import android.media.MediaFormat; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; +import android.os.PersistableBundle; import android.view.Surface; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -306,6 +307,13 @@ import java.nio.ByteBuffer; codec.signalEndOfInputStream(); } + @Override + @RequiresApi(26) + public PersistableBundle getMetrics() { + maybeBlockOnQueueing(); + return codec.getMetrics(); + } + @VisibleForTesting /* package */ void onError(MediaCodec.CodecException error) { asynchronousMediaCodecCallback.onError(codec, error); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java index ffd7758680..3636cf26ed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java @@ -20,6 +20,7 @@ import android.media.MediaCrypto; import android.media.MediaFormat; import android.os.Bundle; import android.os.Handler; +import android.os.PersistableBundle; import android.view.Surface; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -336,4 +337,12 @@ public interface MediaCodecAdapter { */ @RequiresApi(18) void signalEndOfInputStream(); + + /** + * Returns metrics data about the current codec instance. + * + * @see MediaCodec#getMetrics() + */ + @RequiresApi(26) + PersistableBundle getMetrics(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java index 00000fea99..cfe3c4e875 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java @@ -23,6 +23,7 @@ import android.media.MediaCodec; import android.media.MediaFormat; import android.os.Bundle; import android.os.Handler; +import android.os.PersistableBundle; import android.view.Surface; import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; @@ -231,8 +232,16 @@ public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { codec.setVideoScalingMode(scalingMode); } + @Override + @RequiresApi(26) + public PersistableBundle getMetrics() { + return codec.getMetrics(); + } + @RequiresApi(18) private static final class Api18 { + private Api18() {} + @DoNotInline public static Surface createCodecInputSurface(MediaCodec codec) { return codec.createInputSurface(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CapturingRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CapturingRenderersFactory.java index 5540eab1da..1823dac443 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CapturingRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CapturingRenderersFactory.java @@ -23,6 +23,7 @@ import android.media.MediaCodec; import android.media.MediaFormat; import android.os.Bundle; import android.os.Handler; +import android.os.PersistableBundle; import android.util.SparseArray; import android.view.Surface; import androidx.annotation.Nullable; @@ -275,6 +276,12 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa delegate.signalEndOfInputStream(); } + @RequiresApi(26) + @Override + public PersistableBundle getMetrics() { + return delegate.getMetrics(); + } + // Dumpable implementation @Override From 66f7657ac5fcae1676eb691dc129bd37e06b5b50 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 15 Dec 2021 12:04:44 +0000 Subject: [PATCH 18/56] Upgrade dackka to latest build PiperOrigin-RevId: 416521346 --- javadoc_combined.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javadoc_combined.gradle b/javadoc_combined.gradle index 19dabbf167..3336496a26 100644 --- a/javadoc_combined.gradle +++ b/javadoc_combined.gradle @@ -21,7 +21,7 @@ class CombinedJavadocPlugin implements Plugin { // Dackka snapshots are listed at https://androidx.dev/dackka/builds. static final String DACKKA_JAR_URL = - "https://androidx.dev/dackka/builds/7758117/artifacts/dackka-0.0.10.jar" + "https://androidx.dev/dackka/builds/8003564/artifacts/dackka-0.0.14.jar" @Override void apply(Project project) { From 9316b4f49bade15c129095966ca8a526151d901a Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 15 Dec 2021 14:30:00 +0000 Subject: [PATCH 19/56] Add helper method to split the AdPlaybackState PiperOrigin-RevId: 416543473 --- .../android/exoplayer2/ext/ima/ImaUtil.java | 128 +++++ .../exoplayer2/ext/ima/ImaUtilTest.java | 506 ++++++++++++++++++ 2 files changed, 634 insertions(+) create mode 100644 extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java index b750df3625..e6cbac8716 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.ext.ima; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; + import android.content.Context; import android.os.Looper; import android.view.View; @@ -36,6 +39,8 @@ import com.google.ads.interactivemedia.v3.api.UiElement; import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.ui.AdOverlayInfo; import com.google.android.exoplayer2.ui.AdViewProvider; import com.google.android.exoplayer2.upstream.DataSchemeDataSource; @@ -43,10 +48,13 @@ import com.google.android.exoplayer2.upstream.DataSourceUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; /** Utilities for working with IMA SDK and IMA extension data types. */ @@ -255,5 +263,125 @@ import java.util.Set; } } + /** + * Splits an {@link AdPlaybackState} into a separate {@link AdPlaybackState} for each period of a + * content timeline. Ad group times are expected to not take previous ad duration into account and + * needs to be translated to the actual position in the {@code contentTimeline} by adding prior ad + * durations. + * + *

    If a period is enclosed by an ad group, the period is considered an ad period and gets an ad + * playback state assigned with a single ad in a single ad group. The duration of the ad is set to + * the duration of the period. All other periods are considered content periods with an empty ad + * playback state without any ads. + * + * @param adPlaybackState The ad playback state to be split. + * @param contentTimeline The content timeline for each period of which to create an {@link + * AdPlaybackState}. + * @return A map of ad playback states for each period UID in the content timeline. + */ + public static ImmutableMap splitAdPlaybackStateForPeriods( + AdPlaybackState adPlaybackState, Timeline contentTimeline) { + Timeline.Period period = new Timeline.Period(); + if (contentTimeline.getPeriodCount() == 1) { + // A single period gets the entire ad playback state that may contain multiple ad groups. + return ImmutableMap.of( + checkNotNull( + contentTimeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid), + adPlaybackState); + } + + int periodIndex = 0; + long totalElapsedContentDurationUs = 0; + Object adsId = checkNotNull(adPlaybackState.adsId); + AdPlaybackState contentOnlyAdPlaybackState = new AdPlaybackState(adsId); + Map adPlaybackStates = new HashMap<>(); + for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) { + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i); + if (adGroup.timeUs == C.TIME_END_OF_SOURCE) { + checkState(i == adPlaybackState.adGroupCount - 1); + // The last ad group is a placeholder for a potential post roll. We can just stop here. + break; + } + // The ad group start timeUs is in content position. We need to add the ad + // duration before the ad group to translate the start time to the position in the period. + long adGroupDurationUs = getTotalDurationUs(adGroup.durationsUs); + long elapsedAdGroupAdDurationUs = 0; + for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) { + contentTimeline.getPeriod(j, period, /* setIds= */ true); + if (totalElapsedContentDurationUs < adGroup.timeUs) { + // Period starts before the ad group, so it is a content period. + adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState); + totalElapsedContentDurationUs += period.durationUs; + } else { + long periodStartUs = totalElapsedContentDurationUs + elapsedAdGroupAdDurationUs; + if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs) { + // The period ends before the end of the ad group, so it is an ad period (Note: An ad + // reported by the IMA SDK may span multiple periods). + adPlaybackStates.put( + checkNotNull(period.uid), + splitAdGroupForPeriod(adsId, adGroup, periodStartUs, period.durationUs)); + elapsedAdGroupAdDurationUs += period.durationUs; + } else { + // Period is after the current ad group. Continue with next ad group. + break; + } + } + // Increment the period index to the next unclassified period. + periodIndex++; + } + } + // The remaining periods end after the last ad group, so these are content periods. + for (int i = periodIndex; i < contentTimeline.getPeriodCount(); i++) { + contentTimeline.getPeriod(i, period, /* setIds= */ true); + adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState); + } + return ImmutableMap.copyOf(adPlaybackStates); + } + + private static AdPlaybackState splitAdGroupForPeriod( + Object adsId, AdPlaybackState.AdGroup adGroup, long periodStartUs, long periodDurationUs) { + checkState(adGroup.timeUs <= periodStartUs); + AdPlaybackState adPlaybackState = + new AdPlaybackState(checkNotNull(adsId), /* adGroupTimesUs...= */ 0) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + long periodEndUs = periodStartUs + periodDurationUs; + long adDurationsUs = 0; + for (int i = 0; i < adGroup.count; i++) { + adDurationsUs += adGroup.durationsUs[i]; + if (periodEndUs == adGroup.timeUs + adDurationsUs) { + // Map the state of the global ad state to the period specific ad state. + switch (adGroup.states[i]) { + case AdPlaybackState.AD_STATE_PLAYED: + adPlaybackState = + adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + break; + case AdPlaybackState.AD_STATE_SKIPPED: + adPlaybackState = + adPlaybackState.withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + break; + case AdPlaybackState.AD_STATE_ERROR: + adPlaybackState = + adPlaybackState.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + break; + default: + // Do nothing. + break; + } + break; + } + } + return adPlaybackState; + } + + private static long getTotalDurationUs(long[] durationsUs) { + long totalDurationUs = 0; + for (long adDurationUs : durationsUs) { + totalDurationUs += adDurationUs; + } + return totalDurationUs; + } + private ImaUtil() {} } diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java new file mode 100644 index 0000000000..a8a6091327 --- /dev/null +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaUtilTest.java @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2021 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.ext.ima; + +import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US; +import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; +import static com.google.common.truth.Truth.assertThat; + +import android.util.Pair; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ads.AdPlaybackState; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.common.collect.ImmutableMap; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link ImaUtil}. */ +@RunWith(AndroidJUnit4.class) +public class ImaUtilTest { + + @Test + public void splitAdPlaybackStateForPeriods_emptyTimeline_emptyMapOfAdPlaybackStates() { + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "", 0, 20_000, C.TIME_END_OF_SOURCE); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, Timeline.EMPTY); + + assertThat(adPlaybackStates).isEmpty(); + } + + @Test + public void splitAdPlaybackStateForPeriods_singlePeriod_doesNotSplit() { + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "", 0, 20_000, C.TIME_END_OF_SOURCE); + FakeTimeline singlePeriodTimeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, singlePeriodTimeline); + + assertThat(adPlaybackStates).hasSize(1); + assertThat(adPlaybackStates).containsEntry(new Pair<>(0L, 0), adPlaybackState); + } + + @Test + public void splitAdPlaybackStateForPeriods_livePlaceholder_isIgnored() { + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "", C.TIME_END_OF_SOURCE) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline singlePeriodTimeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 3, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, singlePeriodTimeline); + + assertThat(adPlaybackStates).hasSize(3); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0); + } + + @Test + public void splitAdPlaybackStateForPeriods_noAds_splitToEmptyAdPlaybackStates() { + AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ "adsId"); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 11, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(11); + for (AdPlaybackState periodAdPlaybackState : adPlaybackStates.values()) { + assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId"); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(0); + } + } + + @Test + public void splitAdPlaybackStateForPeriods_twoPrerollAds_splitToFirstTwoPeriods() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId", /* adGroupTimesUs... */ 0) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withAdDurationsUs( + /* adGroupIndex= */ 0, + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs, + periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + for (int i = 0; i < 2; i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId"); + assertThat(periodAdPlaybackState.getAdGroup(0).timeUs).isEqualTo(0); + assertThat(periodAdPlaybackState.getAdGroup(0).isServerSideInserted).isTrue(); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + int adDurationUs = i == 0 ? 125_500_000 : 2_500_000; + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(adDurationUs); + } + assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + } + + @Test + public void splitAdPlaybackStateForPeriods_onePrerollAdGroup_splitToFirstThreePeriods() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId", /* adGroupTimesUs... */ 0) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdDurationsUs( + /* adGroupIndex= */ 0, + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 3 * periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + for (int i = 0; i < 3; i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + int adDurationUs = i == 0 ? 125_500_000 : 2_500_000; + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(adDurationUs); + } + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + } + + @Test + public void splitAdPlaybackStateForPeriods_twoMidrollAds_splitToMiddleTwoPeriods() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + for (int i = 1; i < 3; i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.getAdGroup(0).timeUs).isEqualTo(0); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_500_000); + } + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + } + + @Test + public void splitAdPlaybackStateForPeriods_oneMidrollAdGroupOneAd_adSpansTwoPeriods() { + int periodCount = 5; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, 2 * periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + for (int i = 1; i < 3; i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_000_000); + } + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(0); + } + + @Test + public void splitAdPlaybackStateForPeriods_oneMidrollAdGroupTwoAds_eachAdSplitsToOnePeriod() { + int periodCount = 5; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + for (int i = 1; i < 3; i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_000_000); + } + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(0); + } + + @Test + public void splitAdPlaybackStateForPeriods_twoPostrollAds_splitToLastTwoPeriods() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 2 * periodDurationUs) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0); + for (int i = 2; i < periodCount; i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_500_000); + } + } + + @Test + public void splitAdPlaybackStateForPeriods_onePostrollAdGroup_splitToLastThreePeriods() { + int periodCount = 7; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 4 * periodDurationUs) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, 3 * periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + for (int i = 4; i < adPlaybackStates.size(); i++) { + Pair periodUid = new Pair<>(0L, i); + AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid); + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1); + assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(periodDurationUs); + } + } + + @Test + public void splitAdPlaybackStateForPeriods_preMidAndPostrollAdGroup_splitCorrectly() { + int periodCount = 11; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId", 0, (2 * periodDurationUs), (5 * periodDurationUs)) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withAdDurationsUs( + /* adGroupIndex= */ 0, + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs, + periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true) + .withAdCount(/* adGroupIndex= */ 1, 2) + .withAdDurationsUs(/* adGroupIndex= */ 1, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 1, true) + .withAdCount(/* adGroupIndex= */ 2, 2) + .withAdDurationsUs(/* adGroupIndex= */ 2, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 2, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 5)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 6)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 7)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 8)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 9)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 10)).adGroupCount).isEqualTo(1); + } + + @Test + public void splitAdPlaybackStateForPeriods_midAndPostrollAdGroup_splitCorrectly() { + int periodCount = 9; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + (2 * periodDurationUs), + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + (5 * periodDurationUs)) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true) + .withAdCount(/* adGroupIndex= */ 1, 2) + .withAdDurationsUs(/* adGroupIndex= */ 1, periodDurationUs, periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 1, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 5)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 6)).adGroupCount).isEqualTo(0); + assertThat(adPlaybackStates.get(new Pair<>(0L, 7)).adGroupCount).isEqualTo(1); + assertThat(adPlaybackStates.get(new Pair<>(0L, 8)).adGroupCount).isEqualTo(1); + } + + @Test + public void splitAdPlaybackStateForPeriods_correctAdsIdInSplitPlaybackStates() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 2 * periodDurationUs) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, 2 * periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + for (int i = 0; i < adPlaybackStates.size(); i++) { + assertThat(adPlaybackStates.get(new Pair<>(0L, i)).adsId).isEqualTo("adsId"); + } + } + + @Test + public void splitAdPlaybackStateForPeriods_correctAdPlaybackStates() { + int periodCount = 7; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId", 0) + .withAdCount(/* adGroupIndex= */ 0, periodCount) + .withAdDurationsUs( + /* adGroupIndex= */ 0, /* adDurationsUs...= */ + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs, + periodDurationUs, + periodDurationUs, + periodDurationUs, + periodDurationUs, + periodDurationUs, + periodDurationUs) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1) + .withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).getAdGroup(/* adGroupIndex= */ 0).states[0]) + .isEqualTo(AdPlaybackState.AD_STATE_PLAYED); + assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).getAdGroup(/* adGroupIndex= */ 0).states[0]) + .isEqualTo(AdPlaybackState.AD_STATE_SKIPPED); + assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).getAdGroup(/* adGroupIndex= */ 0).states[0]) + .isEqualTo(AdPlaybackState.AD_STATE_ERROR); + assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).getAdGroup(/* adGroupIndex= */ 0).states[0]) + .isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE); + } + + @Test + public void splitAdPlaybackStateForPeriods_lateMidrollAdGroupStartTimeUs_adGroupIgnored() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState( + /* adsId= */ "adsId", + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs + 1) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + for (AdPlaybackState periodAdPlaybackState : adPlaybackStates.values()) { + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(0); + } + } + + @Test + public void splitAdPlaybackStateForPeriods_earlyMidrollAdGroupStartTimeUs_adGroupIgnored() { + int periodCount = 4; + long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ "adsId", periodDurationUs - 1) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs) + .withIsServerSideInserted(/* adGroupIndex= */ 0, true); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ periodCount, /* id= */ 0L)); + + ImmutableMap adPlaybackStates = + ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline); + + assertThat(adPlaybackStates).hasSize(periodCount); + for (AdPlaybackState periodAdPlaybackState : adPlaybackStates.values()) { + assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(0); + assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId"); + } + } +} From 4bf7ffd9a78bbfcec33924606978ef3679942bf6 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 15 Dec 2021 19:56:42 +0000 Subject: [PATCH 20/56] Move ImaAdsLoader Player.Listener implementation to internal class PiperOrigin-RevId: 416613846 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 75 ++++++++++--------- .../exoplayer2/ext/ima/FakeExoPlayer.java | 26 +++++++ .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 26 +------ 3 files changed, 66 insertions(+), 61 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 951053e048..fa1ac96881 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -86,7 +86,7 @@ import java.util.Set; * href="https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/omsdk">IMA * SDK Open Measurement documentation for more information. */ -public final class ImaAdsLoader implements Player.Listener, AdsLoader { +public final class ImaAdsLoader implements AdsLoader { static { ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); @@ -386,6 +386,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader { private final ImaUtil.Configuration configuration; private final Context context; private final ImaUtil.ImaFactory imaFactory; + private final PlayerListenerImpl playerListener; private final HashMap adTagLoaderByAdsId; private final HashMap adTagLoaderByAdsMediaSource; private final Timeline.Period period; @@ -402,6 +403,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader { this.context = context.getApplicationContext(); this.configuration = configuration; this.imaFactory = imaFactory; + playerListener = new PlayerListenerImpl(); supportedMimeTypes = ImmutableList.of(); adTagLoaderByAdsId = new HashMap<>(); adTagLoaderByAdsMediaSource = new HashMap<>(); @@ -532,7 +534,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader { if (player == null) { return; } - player.addListener(this); + player.addListener(playerListener); } @Nullable AdTagLoader adTagLoader = adTagLoaderByAdsId.get(adsId); @@ -554,7 +556,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader { } if (player != null && adTagLoaderByAdsMediaSource.isEmpty()) { - player.removeListener(this); + player.removeListener(playerListener); player = null; } } @@ -562,7 +564,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader { @Override public void release() { if (player != null) { - player.removeListener(this); + player.removeListener(playerListener); player = null; maybeUpdateCurrentAdTagLoader(); } @@ -602,37 +604,6 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader { .handlePrepareError(adGroupIndex, adIndexInAdGroup, exception); } - // Player.Listener implementation. - - @Override - public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { - if (timeline.isEmpty()) { - // The player is being reset or contains no media. - return; - } - maybeUpdateCurrentAdTagLoader(); - maybePreloadNextPeriodAds(); - } - - @Override - public void onPositionDiscontinuity( - Player.PositionInfo oldPosition, - Player.PositionInfo newPosition, - @Player.DiscontinuityReason int reason) { - maybeUpdateCurrentAdTagLoader(); - maybePreloadNextPeriodAds(); - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - maybePreloadNextPeriodAds(); - } - - @Override - public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { - maybePreloadNextPeriodAds(); - } - // Internal methods. private void maybeUpdateCurrentAdTagLoader() { @@ -672,7 +643,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader { } private void maybePreloadNextPeriodAds() { - @Nullable Player player = this.player; + @Nullable Player player = ImaAdsLoader.this.player; if (player == null) { return; } @@ -706,6 +677,38 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader { nextAdTagLoader.maybePreloadAds(Util.usToMs(periodPositionUs), Util.usToMs(period.durationUs)); } + private final class PlayerListenerImpl implements Player.Listener { + + @Override + public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { + if (timeline.isEmpty()) { + // The player is being reset or contains no media. + return; + } + maybeUpdateCurrentAdTagLoader(); + maybePreloadNextPeriodAds(); + } + + @Override + public void onPositionDiscontinuity( + Player.PositionInfo oldPosition, + Player.PositionInfo newPosition, + @Player.DiscontinuityReason int reason) { + maybeUpdateCurrentAdTagLoader(); + maybePreloadNextPeriodAds(); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + maybePreloadNextPeriodAds(); + } + + @Override + public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { + maybePreloadNextPeriodAds(); + } + } + /** * Default {@link ImaUtil.ImaFactory} for non-test usage, which delegates to {@link * ImaSdkFactory}. diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeExoPlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeExoPlayer.java index 90e2087389..18a7854a32 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeExoPlayer.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeExoPlayer.java @@ -15,10 +15,13 @@ */ package com.google.android.exoplayer2.ext.ima; +import static com.google.android.exoplayer2.util.Assertions.checkState; + import android.os.Looper; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.TracksInfo; @@ -182,6 +185,29 @@ import com.google.android.exoplayer2.util.Util; } } + /** + * Sets an error on this player. + * + *

    This will propagate the error to {@link Player.Listener#onPlayerError(PlaybackException)} + * and {@link Player.Listener#onPlayerErrorChanged(PlaybackException)} and will also update the + * state to {@link Player#STATE_IDLE}. + * + *

    The player must be in {@link #STATE_BUFFERING} or {@link #STATE_READY}. + */ + @SuppressWarnings("deprecation") // Calling deprecated listener.onPlayerStateChanged() + public void setPlayerError(PlaybackException error) { + checkState(state == STATE_BUFFERING || state == STATE_READY); + this.state = Player.STATE_IDLE; + listeners.sendEvent( + Player.EVENT_PLAYBACK_STATE_CHANGED, + listener -> { + listener.onPlayerError(error); + listener.onPlayerErrorChanged(error); + listener.onPlayerStateChanged(playWhenReady, state); + listener.onPlaybackStateChanged(state); + }); + } + // ExoPlayer methods. Other methods are unsupported. @Override diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index 0d9e7f042f..37b3c59e15 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -56,7 +56,6 @@ import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; @@ -276,30 +275,7 @@ public final class ImaAdsLoaderTest { ExoPlaybackException anException = ExoPlaybackException.createForSource( new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED); - imaAdsLoader.onPlayerErrorChanged(anException); - imaAdsLoader.onPlayerError(anException); - imaAdsLoader.onPositionDiscontinuity( - new Player.PositionInfo( - /* windowUid= */ new Object(), - /* windowIndex= */ 0, - /* mediaItem= */ MediaItem.fromUri("http://google.com/0"), - /* periodUid= */ new Object(), - /* periodIndex= */ 0, - /* positionMs= */ 10_000, - /* contentPositionMs= */ 0, - /* adGroupIndex= */ -1, - /* adIndexInAdGroup= */ -1), - new Player.PositionInfo( - /* windowUid= */ new Object(), - /* windowIndex= */ 1, - /* mediaItem= */ MediaItem.fromUri("http://google.com/1"), - /* periodUid= */ new Object(), - /* periodIndex= */ 0, - /* positionMs= */ 20_000, - /* contentPositionMs= */ 0, - /* adGroupIndex= */ -1, - /* adIndexInAdGroup= */ -1), - Player.DISCONTINUITY_REASON_SEEK); + fakePlayer.setPlayerError(anException); adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); imaAdsLoader.handlePrepareError( adsMediaSource, /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException()); From 66adeabb1b98a6c899fd63af666f259bc9dfbda6 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Wed, 15 Dec 2021 22:21:58 +0000 Subject: [PATCH 21/56] Use C.LENGTH_UNSET for resolution parameter instead of Format.NO_VALUE. Format.NO_VALUE should only be used for Format fields. PiperOrigin-RevId: 416646415 --- .../google/android/exoplayer2/transformer/FrameEditor.java | 6 +++--- .../google/android/exoplayer2/transformer/Transformer.java | 5 ++--- .../exoplayer2/transformer/TransformerVideoRenderer.java | 2 +- .../android/exoplayer2/transformer/VideoSamplePipeline.java | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java index bce0d98026..6d531237d6 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java @@ -29,7 +29,7 @@ import android.opengl.GLES20; import android.view.Surface; import android.view.SurfaceView; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.GlUtil; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; @@ -109,8 +109,8 @@ import java.util.concurrent.atomic.AtomicInteger; debugPreviewHeight = debugSurfaceView.getHeight(); } else { debugPreviewEglSurface = null; - debugPreviewWidth = Format.NO_VALUE; - debugPreviewHeight = Format.NO_VALUE; + debugPreviewWidth = C.LENGTH_UNSET; + debugPreviewHeight = C.LENGTH_UNSET; } return new FrameEditor( eglDisplay, diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java index 3ceb6710e8..6a0f4ced9a 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java @@ -38,7 +38,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.Player; @@ -113,7 +112,7 @@ public final class Transformer { @Deprecated public Builder() { muxerFactory = new FrameworkMuxer.Factory(); - outputHeight = Format.NO_VALUE; + outputHeight = C.LENGTH_UNSET; transformationMatrix = new Matrix(); containerMimeType = MimeTypes.VIDEO_MP4; listener = new Listener() {}; @@ -130,7 +129,7 @@ public final class Transformer { public Builder(Context context) { this.context = context.getApplicationContext(); muxerFactory = new FrameworkMuxer.Factory(); - outputHeight = Format.NO_VALUE; + outputHeight = C.LENGTH_UNSET; transformationMatrix = new Matrix(); containerMimeType = MimeTypes.VIDEO_MP4; listener = new Listener() {}; diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java index f88cc1745b..b70fae078a 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java @@ -89,7 +89,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; && !transformation.videoMimeType.equals(inputFormat.sampleMimeType)) { return true; } - if (transformation.outputHeight != Format.NO_VALUE + if (transformation.outputHeight != C.LENGTH_UNSET && transformation.outputHeight != inputFormat.height) { return true; } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java index 1fc359b4c9..1c621e96d1 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java @@ -66,7 +66,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // TODO(internal b/209781577): Think about which edge length should be set for portrait videos. int outputWidth = inputFormat.width; int outputHeight = inputFormat.height; - if (transformation.outputHeight != Format.NO_VALUE + if (transformation.outputHeight != C.LENGTH_UNSET && transformation.outputHeight != inputFormat.height) { outputWidth = inputFormat.width * transformation.outputHeight / inputFormat.height; outputHeight = transformation.outputHeight; From 8bb53b409a7a902a21e1d4e52d721be945809dc9 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Thu, 16 Dec 2021 11:04:35 +0000 Subject: [PATCH 22/56] Remove ExoPlaybackException dependency from sample pipelines. Use TransformationException for codec and audio processor initialization problems instead. PiperOrigin-RevId: 416765510 --- .../transformer/AudioSamplePipeline.java | 73 +++------ .../transformer/MediaCodecAdapterWrapper.java | 154 ++++++++++-------- .../transformer/SamplePipeline.java | 3 +- .../transformer/TransformationException.java | 43 ++++- .../exoplayer2/transformer/Transformer.java | 37 ++--- .../transformer/TransformerAudioRenderer.java | 5 +- .../transformer/TransformerBaseRenderer.java | 24 ++- .../transformer/TransformerVideoRenderer.java | 6 +- .../transformer/VideoSamplePipeline.java | 65 +++----- .../transformer/TransformerTest.java | 86 ++++++++++ 10 files changed, 292 insertions(+), 204 deletions(-) diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java index fd9acd502d..7b108d06b3 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java @@ -24,14 +24,11 @@ import static java.lang.Math.min; import android.media.MediaCodec.BufferInfo; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; import com.google.android.exoplayer2.audio.SonicAudioProcessor; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import java.io.IOException; import java.nio.ByteBuffer; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -47,7 +44,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final Format inputFormat; private final Transformation transformation; - private final int rendererIndex; private final MediaCodecAdapterWrapper decoder; private final DecoderInputBuffer decoderInputBuffer; @@ -67,11 +63,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private boolean drainingSonicForSpeedChange; private float currentSpeed; - public AudioSamplePipeline(Format inputFormat, Transformation transformation, int rendererIndex) - throws ExoPlaybackException { + public AudioSamplePipeline(Format inputFormat, Transformation transformation) + throws TransformationException { this.inputFormat = inputFormat; this.transformation = transformation; - this.rendererIndex = rendererIndex; decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); encoderInputBuffer = @@ -82,19 +77,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER; speedProvider = new SegmentSpeedProvider(inputFormat); currentSpeed = speedProvider.getSpeed(0); - try { - this.decoder = MediaCodecAdapterWrapper.createForAudioDecoding(inputFormat); - } catch (IOException e) { - // TODO(internal b/192864511): Assign a specific error code. - throw ExoPlaybackException.createForRenderer( - e, - TAG, - rendererIndex, - inputFormat, - /* rendererFormatSupport= */ C.FORMAT_HANDLED, - /* isRecoverable= */ false, - PlaybackException.ERROR_CODE_UNSPECIFIED); - } + this.decoder = MediaCodecAdapterWrapper.createForAudioDecoding(inputFormat); } @Override @@ -109,7 +92,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - public boolean processData() throws ExoPlaybackException { + public boolean processData() throws TransformationException { if (!ensureEncoderAndAudioProcessingConfigured()) { return false; } @@ -292,7 +275,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @EnsuresNonNullIf( expression = {"encoder", "encoderInputAudioFormat"}, result = true) - private boolean ensureEncoderAndAudioProcessingConfigured() throws ExoPlaybackException { + private boolean ensureEncoderAndAudioProcessingConfigured() throws TransformationException { if (encoder != null && encoderInputAudioFormat != null) { return true; } @@ -310,27 +293,24 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; outputAudioFormat = sonicAudioProcessor.configure(outputAudioFormat); flushSonicAndSetSpeed(currentSpeed); } catch (AudioProcessor.UnhandledAudioFormatException e) { - // TODO(internal b/192864511): Assign an adequate error code. - throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED); + throw TransformationException.createForAudioProcessor( + e, + "Sonic", + outputAudioFormat, + TransformationException.ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED); } } - String audioMimeType = - transformation.audioMimeType == null - ? inputFormat.sampleMimeType - : transformation.audioMimeType; - try { - encoder = - MediaCodecAdapterWrapper.createForAudioEncoding( - new Format.Builder() - .setSampleMimeType(audioMimeType) - .setSampleRate(outputAudioFormat.sampleRate) - .setChannelCount(outputAudioFormat.channelCount) - .setAverageBitrate(DEFAULT_ENCODER_BITRATE) - .build()); - } catch (IOException e) { - // TODO(internal b/192864511): Assign an adequate error code. - throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED); - } + encoder = + MediaCodecAdapterWrapper.createForAudioEncoding( + new Format.Builder() + .setSampleMimeType( + transformation.audioMimeType == null + ? inputFormat.sampleMimeType + : transformation.audioMimeType) + .setSampleRate(outputAudioFormat.sampleRate) + .setChannelCount(outputAudioFormat.channelCount) + .setAverageBitrate(DEFAULT_ENCODER_BITRATE) + .build()); encoderInputAudioFormat = outputAudioFormat; return true; } @@ -351,17 +331,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; sonicAudioProcessor.flush(); } - private ExoPlaybackException createRendererException(Throwable cause, int errorCode) { - return ExoPlaybackException.createForRenderer( - cause, - TAG, - rendererIndex, - inputFormat, - /* rendererFormatSupport= */ C.FORMAT_HANDLED, - /* isRecoverable= */ false, - errorCode); - } - private void computeNextEncoderInputBufferTimeUs( long bytesWritten, int bytesPerFrame, int sampleRate) { // The calculation below accounts for remainders and rounding. Without that it corresponds to diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MediaCodecAdapterWrapper.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MediaCodecAdapterWrapper.java index 0ca2be339c..f0e11b698a 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MediaCodecAdapterWrapper.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MediaCodecAdapterWrapper.java @@ -100,29 +100,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @param format The {@link Format} (of the input data) used to determine the underlying {@link * MediaCodec} and its configuration values. * @return A configured and started decoder wrapper. - * @throws IOException If the underlying codec cannot be created. + * @throws TransformationException If the underlying codec cannot be created. */ - public static MediaCodecAdapterWrapper createForAudioDecoding(Format format) throws IOException { - @Nullable MediaCodecAdapter adapter = null; + public static MediaCodecAdapterWrapper createForAudioDecoding(Format format) + throws TransformationException { + MediaFormat mediaFormat = + MediaFormat.createAudioFormat( + checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); + MediaFormatUtil.maybeSetInteger( + mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); + MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); + + MediaCodecAdapter adapter; try { - MediaFormat mediaFormat = - MediaFormat.createAudioFormat( - checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); - MediaFormatUtil.maybeSetInteger( - mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); - MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); adapter = new Factory() .createAdapter( MediaCodecAdapter.Configuration.createForAudioDecoding( createPlaceholderMediaCodecInfo(), mediaFormat, format, /* crypto= */ null)); - return new MediaCodecAdapterWrapper(adapter); } catch (Exception e) { - if (adapter != null) { - adapter.release(); - } - throw e; + throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ true); } + return new MediaCodecAdapterWrapper(adapter); } /** @@ -133,28 +132,26 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * MediaCodec} and its configuration values. * @param surface The {@link Surface} to which the decoder output is rendered. * @return A configured and started decoder wrapper. - * @throws IOException If the underlying codec cannot be created. + * @throws TransformationException If the underlying codec cannot be created. */ @SuppressLint("InlinedApi") public static MediaCodecAdapterWrapper createForVideoDecoding(Format format, Surface surface) - throws IOException { - @Nullable MediaCodecAdapter adapter = null; + throws TransformationException { + MediaFormat mediaFormat = + MediaFormat.createVideoFormat( + checkNotNull(format.sampleMimeType), format.width, format.height); + MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees); + MediaFormatUtil.maybeSetInteger( + mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); + MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); + if (SDK_INT >= 29) { + // On API levels over 29, Transformer decodes as many frames as possible in one render + // cycle. This key ensures no frame dropping when the decoder's output surface is full. + mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0); + } + + MediaCodecAdapter adapter; try { - MediaFormat mediaFormat = - MediaFormat.createVideoFormat( - checkNotNull(format.sampleMimeType), format.width, format.height); - MediaFormatUtil.maybeSetInteger( - mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees); - MediaFormatUtil.maybeSetInteger( - mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); - MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); - - if (SDK_INT >= 29) { - // On API levels over 29, Transformer decodes as many frames as possible in one render - // cycle. This key ensures no frame dropping when the decoder's output surface is full. - mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0); - } - adapter = new Factory() .createAdapter( @@ -164,13 +161,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; format, surface, /* crypto= */ null)); - return new MediaCodecAdapterWrapper(adapter); } catch (Exception e) { - if (adapter != null) { - adapter.release(); - } - throw e; + throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ true); } + return new MediaCodecAdapterWrapper(adapter); } /** @@ -180,30 +174,26 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @param format The {@link Format} (of the output data) used to determine the underlying {@link * MediaCodec} and its configuration values. * @return A configured and started encoder wrapper. - * @throws IOException If the underlying codec cannot be created. + * @throws TransformationException If the underlying codec cannot be created. */ - public static MediaCodecAdapterWrapper createForAudioEncoding(Format format) throws IOException { - @Nullable MediaCodec encoder = null; - @Nullable MediaCodecAdapter adapter = null; + public static MediaCodecAdapterWrapper createForAudioEncoding(Format format) + throws TransformationException { + MediaFormat mediaFormat = + MediaFormat.createAudioFormat( + checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate); + + MediaCodecAdapter adapter; try { - MediaFormat mediaFormat = - MediaFormat.createAudioFormat( - checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); - mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate); adapter = new Factory() .createAdapter( MediaCodecAdapter.Configuration.createForAudioEncoding( createPlaceholderMediaCodecInfo(), mediaFormat, format)); - return new MediaCodecAdapterWrapper(adapter); } catch (Exception e) { - if (adapter != null) { - adapter.release(); - } else if (encoder != null) { - encoder.release(); - } - throw e; + throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ false); } + return new MediaCodecAdapterWrapper(adapter); } /** @@ -219,41 +209,37 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * are from {@code MediaFormat.KEY_*} constants. Its values will override those in {@code * format}. * @return A configured and started encoder wrapper. - * @throws IOException If the underlying codec cannot be created. + * @throws TransformationException If the underlying codec cannot be created. */ public static MediaCodecAdapterWrapper createForVideoEncoding( - Format format, Map additionalEncoderConfig) throws IOException { + Format format, Map additionalEncoderConfig) throws TransformationException { checkArgument(format.width != Format.NO_VALUE); checkArgument(format.height != Format.NO_VALUE); checkArgument(format.height < format.width); checkArgument(format.rotationDegrees == 0); - @Nullable MediaCodecAdapter adapter = null; + MediaFormat mediaFormat = + MediaFormat.createVideoFormat( + checkNotNull(format.sampleMimeType), format.width, format.height); + mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface); + mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30); + mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 413_000); + for (Map.Entry encoderSetting : additionalEncoderConfig.entrySet()) { + mediaFormat.setInteger(encoderSetting.getKey(), encoderSetting.getValue()); + } + + MediaCodecAdapter adapter; try { - MediaFormat mediaFormat = - MediaFormat.createVideoFormat( - checkNotNull(format.sampleMimeType), format.width, format.height); - mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface); - mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30); - mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); - mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 413_000); - - for (Map.Entry encoderSetting : additionalEncoderConfig.entrySet()) { - mediaFormat.setInteger(encoderSetting.getKey(), encoderSetting.getValue()); - } - adapter = new Factory() .createAdapter( MediaCodecAdapter.Configuration.createForVideoEncoding( createPlaceholderMediaCodecInfo(), mediaFormat, format)); - return new MediaCodecAdapterWrapper(adapter); } catch (Exception e) { - if (adapter != null) { - adapter.release(); - } - throw e; + throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ false); } + return new MediaCodecAdapterWrapper(adapter); } private MediaCodecAdapterWrapper(MediaCodecAdapter codec) { @@ -458,4 +444,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } return formatBuilder.build(); } + + private static TransformationException createTransformationException( + Exception cause, Format format, boolean isVideo, boolean isDecoder) { + String componentName = (isVideo ? "Video" : "Audio") + (isDecoder ? "Decoder" : "Encoder"); + if (cause instanceof IOException) { + return TransformationException.createForCodec( + cause, + componentName, + format, + isDecoder + ? TransformationException.ERROR_CODE_DECODER_INIT_FAILED + : TransformationException.ERROR_CODE_ENCODER_INIT_FAILED); + } + if (cause instanceof IllegalArgumentException) { + return TransformationException.createForCodec( + cause, + componentName, + format, + isDecoder + ? TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED + : TransformationException.ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED); + } + return TransformationException.createForUnexpected(cause); + } } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SamplePipeline.java index e2bf021dd2..62c94ee6c2 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SamplePipeline.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.transformer; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; @@ -44,7 +43,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; * Processes the input data and returns whether more data can be processed by calling this method * again. */ - boolean processData() throws ExoPlaybackException; + boolean processData() throws TransformationException; /** Returns the output format of the pipeline if available, and {@code null} otherwise. */ @Nullable diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java index 705a026f34..03909481fb 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java @@ -24,6 +24,9 @@ import static java.lang.annotation.ElementType.TYPE_USE; import android.os.SystemClock; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.audio.AudioProcessor; +import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; @@ -34,6 +37,36 @@ import java.lang.annotation.Target; /** Thrown when a non-locally recoverable transformation failure occurs. */ public final class TransformationException extends Exception { + /** + * Creates an instance for a decoder or encoder related exception. + * + * @param cause The cause of the failure. + * @param componentName The name of the component used, e.g. 'VideoEncoder'. + * @param format The {@link Format} used for the decoder/encoder. + * @param errorCode See {@link #errorCode}. + * @return The created instance. + */ + public static TransformationException createForCodec( + Throwable cause, String componentName, Format format, int errorCode) { + return new TransformationException( + componentName + " error, format = " + format, cause, errorCode); + } + + /** + * Creates an instance for an audio processing related exception. + * + * @param cause The cause of the failure. + * @param componentName The name of the {@link AudioProcessor} used. + * @param audioFormat The {@link AudioFormat} used. + * @param errorCode See {@link #errorCode}. + * @return The created instance. + */ + public static TransformationException createForAudioProcessor( + Throwable cause, String componentName, AudioFormat audioFormat, int errorCode) { + return new TransformationException( + componentName + " error, audio_format = " + audioFormat, cause, errorCode); + } + /** * Creates an instance for an unexpected exception. * @@ -72,6 +105,7 @@ public final class TransformationException extends Exception { ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED, ERROR_CODE_GL_INIT_FAILED, ERROR_CODE_GL_PROCESSING_FAILED, + ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED, }) public @interface ErrorCode {} @@ -104,13 +138,18 @@ public final class TransformationException extends Exception { /** Caused by requesting to encode content in a format that is not supported by the device. */ public static final int ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED = 3003; - // GL errors (4xxx). + // Video editing errors (4xxx). /** Caused by a GL initialization failure. */ public static final int ERROR_CODE_GL_INIT_FAILED = 4001; /** Caused by a failure while using or releasing a GL program. */ public static final int ERROR_CODE_GL_PROCESSING_FAILED = 4002; + // Audio editing errors (5xxx). + + /** Caused by an audio processor initialization failure. */ + public static final int ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED = 5001; + /** Returns the name of a given {@code errorCode}. */ public static String getErrorCodeName(@ErrorCode int errorCode) { switch (errorCode) { @@ -134,6 +173,8 @@ public final class TransformationException extends Exception { return "ERROR_CODE_GL_INIT_FAILED"; case ERROR_CODE_GL_PROCESSING_FAILED: return "ERROR_CODE_GL_PROCESSING_FAILED"; + case ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED: + return "ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED"; default: return "invalid error code"; } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java index 6a0f4ced9a..b1688ee62b 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java @@ -829,29 +829,33 @@ public final class Transformer { @Override public void onTracksInfoChanged(TracksInfo tracksInfo) { if (muxerWrapper.getTrackCount() == 0) { + // TODO(b/209469847): Do not silently drop unsupported tracks and throw a more specific + // exception earlier. handleTransformationEnded( - new IllegalStateException( - "The output does not contain any tracks. Check that at least one of the input" - + " sample formats is supported.")); + TransformationException.createForUnexpected( + new IllegalStateException( + "The output does not contain any tracks. Check that at least one of the input" + + " sample formats is supported."))); } } @Override public void onPlayerError(PlaybackException error) { - // TODO(internal b/209469847): Once TransformationException is used in transformer components, - // extract TransformationExceptions wrapped in the PlaybackExceptions here before passing them - // on. - handleTransformationEnded(error); + Throwable cause = error.getCause(); + handleTransformationEnded( + cause instanceof TransformationException + ? (TransformationException) cause + : TransformationException.createForUnexpected(error)); } - private void handleTransformationEnded(@Nullable Exception exception) { - @Nullable Exception resourceReleaseException = null; + private void handleTransformationEnded(@Nullable TransformationException exception) { + @Nullable TransformationException resourceReleaseException = null; try { releaseResources(/* forCancellation= */ false); } catch (IllegalStateException e) { - // TODO(internal b/209469847): Use a TransformationException with a specific error code when - // the IllegalStateException is caused by the muxer. - resourceReleaseException = e; + // TODO(internal b/209469847): Use a more specific error code when the IllegalStateException + // is caused by the muxer. + resourceReleaseException = TransformationException.createForUnexpected(e); } if (exception == null && resourceReleaseException == null) { @@ -860,15 +864,10 @@ public final class Transformer { } if (exception != null) { - listener.onTransformationError( - mediaItem, - exception instanceof TransformationException - ? exception - : TransformationException.createForUnexpected(exception)); + listener.onTransformationError(mediaItem, exception); } if (resourceReleaseException != null) { - listener.onTransformationError( - mediaItem, TransformationException.createForUnexpected(resourceReleaseException)); + listener.onTransformationError(mediaItem, resourceReleaseException); } } } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java index 1101ff067e..846aa090ef 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java @@ -21,7 +21,6 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; @@ -49,7 +48,7 @@ import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; /** Attempts to read the input format and to initialize the {@link SamplePipeline}. */ @Override - protected boolean ensureConfigured() throws ExoPlaybackException { + protected boolean ensureConfigured() throws TransformationException { if (samplePipeline != null) { return true; } @@ -61,7 +60,7 @@ import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; } Format inputFormat = checkNotNull(formatHolder.format); if (shouldTranscode(inputFormat)) { - samplePipeline = new AudioSamplePipeline(inputFormat, transformation, getIndex()); + samplePipeline = new AudioSamplePipeline(inputFormat, transformation); } else { samplePipeline = new PassthroughSamplePipeline(inputFormat); } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java index 443ace640d..ba36131f85 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; @@ -95,11 +96,24 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override public final void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (!isRendererStarted || isEnded() || !ensureConfigured()) { - return; - } + try { + if (!isRendererStarted || isEnded() || !ensureConfigured()) { + return; + } - while (feedMuxerFromPipeline() || samplePipeline.processData() || feedPipelineFromInput()) {} + while (feedMuxerFromPipeline() || samplePipeline.processData() || feedPipelineFromInput()) {} + } catch (TransformationException e) { + // Transformer extracts the TransformationException from this ExoPlaybackException again. This + // temporary wrapping is needed due to the dependence on ExoPlayer's BaseRenderer. + throw ExoPlaybackException.createForRenderer( + e, + "Transformer", + getIndex(), + /* rendererFormat= */ null, + C.FORMAT_HANDLED, + /* isRecoverable= */ false, + PlaybackException.ERROR_CODE_UNSPECIFIED); + } } @Override @@ -134,7 +148,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @ForOverride @EnsuresNonNullIf(expression = "samplePipeline", result = true) - protected abstract boolean ensureConfigured() throws ExoPlaybackException; + protected abstract boolean ensureConfigured() throws TransformationException; @RequiresNonNull({"samplePipeline", "#1.data"}) protected void maybeQueueSampleToPipeline(DecoderInputBuffer inputBuffer) { diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java index b70fae078a..dccd1d2ca3 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java @@ -21,7 +21,6 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import android.content.Context; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; @@ -60,7 +59,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** Attempts to read the input format and to initialize the {@link SamplePipeline}. */ @Override - protected boolean ensureConfigured() throws ExoPlaybackException { + protected boolean ensureConfigured() throws TransformationException { if (samplePipeline != null) { return true; } @@ -73,8 +72,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; Format inputFormat = checkNotNull(formatHolder.format); if (shouldTranscode(inputFormat)) { samplePipeline = - new VideoSamplePipeline( - context, inputFormat, transformation, getIndex(), debugViewProvider); + new VideoSamplePipeline(context, inputFormat, transformation, debugViewProvider); } else { samplePipeline = new PassthroughSamplePipeline(inputFormat); } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java index 1c621e96d1..eab7aa803e 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java @@ -25,12 +25,9 @@ import android.media.MediaFormat; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.common.collect.ImmutableMap; -import java.io.IOException; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -55,9 +52,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Context context, Format inputFormat, Transformation transformation, - int rendererIndex, Transformer.DebugViewProvider debugViewProvider) - throws ExoPlaybackException { + throws TransformationException { decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); encoderOutputBuffer = @@ -88,24 +84,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // postrotation in a later vertex shader. transformation.transformationMatrix.postRotate(outputRotationDegrees); - try { - encoder = - MediaCodecAdapterWrapper.createForVideoEncoding( - new Format.Builder() - .setWidth(outputWidth) - .setHeight(outputHeight) - .setRotationDegrees(0) - .setSampleMimeType( - transformation.videoMimeType != null - ? transformation.videoMimeType - : inputFormat.sampleMimeType) - .build(), - ImmutableMap.of()); - } catch (IOException e) { - // TODO(internal b/192864511): Assign a specific error code. - throw createRendererException( - e, rendererIndex, inputFormat, PlaybackException.ERROR_CODE_UNSPECIFIED); - } + encoder = + MediaCodecAdapterWrapper.createForVideoEncoding( + new Format.Builder() + .setWidth(outputWidth) + .setHeight(outputHeight) + .setRotationDegrees(0) + .setSampleMimeType( + transformation.videoMimeType != null + ? transformation.videoMimeType + : inputFormat.sampleMimeType) + .build(), + ImmutableMap.of()); if (inputFormat.height != outputHeight || inputFormat.width != outputWidth || !transformation.transformationMatrix.isIdentity()) { @@ -118,17 +108,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /* outputSurface= */ checkNotNull(encoder.getInputSurface()), debugViewProvider); } - try { - decoder = - MediaCodecAdapterWrapper.createForVideoDecoding( - inputFormat, - frameEditor == null - ? checkNotNull(encoder.getInputSurface()) - : frameEditor.getInputSurface()); - } catch (IOException e) { - throw createRendererException( - e, rendererIndex, inputFormat, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED); - } + decoder = + MediaCodecAdapterWrapper.createForVideoDecoding( + inputFormat, + frameEditor == null + ? checkNotNull(encoder.getInputSurface()) + : frameEditor.getInputSurface()); } @Override @@ -257,16 +242,4 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; decoder.release(); encoder.release(); } - - private static ExoPlaybackException createRendererException( - Throwable cause, int rendererIndex, Format inputFormat, int errorCode) { - return ExoPlaybackException.createForRenderer( - cause, - TAG, - rendererIndex, - inputFormat, - /* rendererFormatSupport= */ C.FORMAT_HANDLED, - /* isRecoverable= */ false, - errorCode); - } } diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java index 805b2ae892..6bb8925bba 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java @@ -24,11 +24,15 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import android.content.Context; +import android.media.MediaCrypto; +import android.media.MediaFormat; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; +import android.view.Surface; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.ExoPlaybackException; @@ -39,6 +43,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.Iterables; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; @@ -232,6 +237,63 @@ public final class TransformerTest { DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_WITH_SEF_SLOW_MOTION)); } + @Test + public void startTransformation_withAudioEncoderFormatUnsupported_completesWithError() + throws Exception { + Transformer transformer = + new Transformer.Builder(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .setAudioMimeType(MimeTypes.AUDIO_AMR_WB) // unsupported encoder MIME type + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_ONLY); + + transformer.startTransformation(mediaItem, outputPath); + TransformationException exception = TransformerTestRunner.runUntilError(transformer); + + assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(exception.errorCode) + .isEqualTo(TransformationException.ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED); + } + + @Test + public void startTransformation_withAudioDecoderFormatUnsupported_completesWithError() + throws Exception { + Transformer transformer = + new Transformer.Builder(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .setAudioMimeType(MimeTypes.AUDIO_AAC) // supported encoder MIME type + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_WITH_ALL_SAMPLE_FORMATS_UNSUPPORTED); + + transformer.startTransformation(mediaItem, outputPath); + TransformationException exception = TransformerTestRunner.runUntilError(transformer); + + assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(exception.errorCode) + .isEqualTo(TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED); + } + + @Test + public void startTransformation_withVideoEncoderFormatUnsupported_completesWithError() + throws Exception { + Transformer transformer = + new Transformer.Builder(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .setVideoMimeType(MimeTypes.VIDEO_H263) // unsupported encoder MIME type + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); + + transformer.startTransformation(mediaItem, outputPath); + TransformationException exception = TransformerTestRunner.runUntilError(transformer); + + assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(exception.errorCode) + .isEqualTo(TransformationException.ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED); + } + @Test public void startTransformation_withPlayerError_completesWithError() throws Exception { Transformer transformer = new Transformer.Builder(context).setClock(clock).build(); @@ -541,6 +603,30 @@ public final class TransformerTest { ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AAC, codecConfig); ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AMR_NB, codecConfig); ShadowMediaCodec.addEncoder(MimeTypes.AUDIO_AAC, codecConfig); + + ShadowMediaCodec.CodecConfig throwingCodecConfig = + new ShadowMediaCodec.CodecConfig( + /* inputBufferSize= */ 10_000, + /* outputBufferSize= */ 10_000, + new ShadowMediaCodec.CodecConfig.Codec() { + + @Override + public void process(ByteBuffer in, ByteBuffer out) { + out.put(in); + } + + @Override + public void onConfigured( + MediaFormat format, + @Nullable Surface surface, + @Nullable MediaCrypto crypto, + int flags) { + throw new IllegalArgumentException("Format unsupported"); + } + }); + ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AC3, throwingCodecConfig); + ShadowMediaCodec.addEncoder(MimeTypes.AUDIO_AMR_WB, throwingCodecConfig); + ShadowMediaCodec.addEncoder(MimeTypes.VIDEO_H263, throwingCodecConfig); } private static void removeEncodersAndDecoders() { From 9d463725fb33523e9c15fae8cf0d7ac3c86e9d6c Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 16 Dec 2021 14:00:05 +0000 Subject: [PATCH 23/56] Exclude last chunk when applying fraction for live quality increase We check the fraction of the available duration we have already buffered for live streams to see if we can increase the quality. This fraction compares against the overall available media duration at the time of the track selection, which by definition can't include one of the availabe chunks (as this is the one we want to load next). That means, for example, that for a reasonable live offset of 3 segments we can at most reach a fraction of 0.66, which is less than our default threshold of 0.75, meaning we can never switch up. By subtracting one chunk duration from the available duration, we make this comparison fair again and allow all live streams (regardless of live offset) to reach up to 100% buffered data (which is above our default value of 75%), so that they can increase the quality. Issue: google/ExoPlayer#9784 PiperOrigin-RevId: 416791033 --- RELEASENOTES.md | 3 ++ .../AdaptiveTrackSelection.java | 21 ++++++-- .../AdaptiveTrackSelectionTest.java | 54 +++++++++++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5f03454544..fa7fea5868 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,6 +21,9 @@ * Add `MediaCodecAdapter.getMetrics()` to allow users obtain metrics data from `MediaCodec`. ([#9766](https://github.com/google/ExoPlayer/issues/9766)). + * Amend logic in `AdaptiveTrackSelection` to allow a quality increase + under sufficient network bandwidth even if playback is very close to the + live edge ((#9784)[https://github.com/google/ExoPlayer/issues/9784]). * Android 12 compatibility: * Upgrade the Cast extension to depend on `com.google.android.gms:play-services-cast-framework:20.1.0`. Earlier diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index 918888dff6..5c95b0c320 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -458,8 +458,10 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { // Revert back to the previous selection if conditions are not suitable for switching. Format currentFormat = getFormat(previousSelectedIndex); Format selectedFormat = getFormat(newSelectedIndex); + long minDurationForQualityIncreaseUs = + minDurationForQualityIncreaseUs(availableDurationUs, chunkDurationUs); if (selectedFormat.bitrate > currentFormat.bitrate - && bufferedDurationUs < minDurationForQualityIncreaseUs(availableDurationUs)) { + && bufferedDurationUs < minDurationForQualityIncreaseUs) { // The selected track is a higher quality, but we have insufficient buffer to safely switch // up. Defer switching up for now. newSelectedIndex = previousSelectedIndex; @@ -599,13 +601,22 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { return lowestBitrateAllowedIndex; } - private long minDurationForQualityIncreaseUs(long availableDurationUs) { + private long minDurationForQualityIncreaseUs(long availableDurationUs, long chunkDurationUs) { boolean isAvailableDurationTooShort = availableDurationUs != C.TIME_UNSET && availableDurationUs <= minDurationForQualityIncreaseUs; - return isAvailableDurationTooShort - ? (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease) - : minDurationForQualityIncreaseUs; + if (!isAvailableDurationTooShort) { + return minDurationForQualityIncreaseUs; + } + if (chunkDurationUs != C.TIME_UNSET) { + // We are currently selecting a new live chunk. Even under perfect conditions, the buffered + // duration can't include the last chunk duration yet because we are still selecting a track + // for this or a previous chunk. Hence, we subtract one chunk duration from the total + // available live duration to ensure we only compare the buffered duration against what is + // actually achievable. + availableDurationUs -= chunkDurationUs; + } + return (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease); } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java index 8059d0731e..6b453b5f5a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java @@ -163,6 +163,40 @@ public final class AdaptiveTrackSelectionTest { assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); } + @Test + public void updateSelectedTrack_liveStream_switchesUpWhenBufferedFractionToLiveEdgeReached() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + // The second measurement onward returns 2000L, which prompts the track selection to switch up + // if possible. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L, 2000L); + AdaptiveTrackSelection adaptiveTrackSelection = + prepareAdaptiveTrackSelectionWithBufferedFractionToLiveEdgeForQualiyIncrease( + trackGroup, /* bufferedFractionToLiveEdgeForQualityIncrease= */ 0.75f); + + // Not buffered close to live edge yet. + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 1_600_000, + /* availableDurationUs= */ 5_600_000, + /* queue= */ ImmutableList.of(), + createMediaChunkIterators(trackGroup, /* chunkDurationUs= */ 2_000_000)); + + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2); + + // Buffered all possible chunks (except for newly added chunk of 2 seconds). + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 3_600_000, + /* availableDurationUs= */ 5_600_000, + /* queue= */ ImmutableList.of(), + createMediaChunkIterators(trackGroup, /* chunkDurationUs= */ 2_000_000)); + + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format3); + } + @Test public void updateSelectedTrackDoNotSwitchDownIfBufferedEnough() { Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); @@ -731,6 +765,26 @@ public final class AdaptiveTrackSelectionTest { fakeClock)); } + private AdaptiveTrackSelection + prepareAdaptiveTrackSelectionWithBufferedFractionToLiveEdgeForQualiyIncrease( + TrackGroup trackGroup, float bufferedFractionToLiveEdgeForQualityIncrease) { + return prepareTrackSelection( + new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + TrackSelection.TYPE_UNSET, + mockBandwidthMeter, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + AdaptiveTrackSelection.DEFAULT_MAX_WIDTH_TO_DISCARD, + AdaptiveTrackSelection.DEFAULT_MAX_HEIGHT_TO_DISCARD, + /* bandwidthFraction= */ 1.0f, + bufferedFractionToLiveEdgeForQualityIncrease, + /* adaptationCheckpoints= */ ImmutableList.of(), + fakeClock)); + } + private AdaptiveTrackSelection prepareAdaptiveTrackSelectionWithAdaptationCheckpoints( TrackGroup trackGroup, List adaptationCheckpoints) { return prepareTrackSelection( From f2d337c33d82a69620bd883c27cdedeb15807341 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Thu, 16 Dec 2021 14:13:54 +0000 Subject: [PATCH 24/56] Convert PlaybackExceptions to TransformationExceptions. Transformer uses ExoPlayer for reading input. Apps using Transformer do not need to know this. So, PlaybackExceptions are converted to TransformationExceptions with the same message, cause and error code. The corresponding IO error codes are copied from PlaybackException. PiperOrigin-RevId: 416793741 --- .../transformer/TransformationException.java | 275 +++++++++++------- .../exoplayer2/transformer/Transformer.java | 2 +- .../transformer/TransformerTest.java | 6 +- 3 files changed, 177 insertions(+), 106 deletions(-) diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java index 03909481fb..524802aa02 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java @@ -25,10 +25,12 @@ import android.os.SystemClock; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableBiMap; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -37,6 +39,167 @@ import java.lang.annotation.Target; /** Thrown when a non-locally recoverable transformation failure occurs. */ public final class TransformationException extends Exception { + /** + * Codes that identify causes of {@link Transformer} errors. + * + *

    This list of errors may be extended in future versions. The underlying values may also + * change, so it is best to avoid relying on them directly without using the constants. + */ + // TODO(b/209469847): Update the javadoc once the underlying values are fixed. + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) + @IntDef( + open = true, + value = { + ERROR_CODE_UNSPECIFIED, + ERROR_CODE_FAILED_RUNTIME_CHECK, + ERROR_CODE_IO_UNSPECIFIED, + ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, + ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, + ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, + ERROR_CODE_IO_BAD_HTTP_STATUS, + ERROR_CODE_IO_FILE_NOT_FOUND, + ERROR_CODE_IO_NO_PERMISSION, + ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, + ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, + ERROR_CODE_DECODER_INIT_FAILED, + ERROR_CODE_DECODING_FAILED, + ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, + ERROR_CODE_ENCODER_INIT_FAILED, + ERROR_CODE_ENCODING_FAILED, + ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED, + ERROR_CODE_GL_INIT_FAILED, + ERROR_CODE_GL_PROCESSING_FAILED, + ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED, + }) + public @interface ErrorCode {} + + // Miscellaneous errors (1xxx). + + /** Caused by an error whose cause could not be identified. */ + public static final int ERROR_CODE_UNSPECIFIED = 1000; + /** + * Caused by a failed runtime check. + * + *

    This can happen when transformer reaches an invalid state. + */ + public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 1001; + + // Input/Output errors (2xxx). + + /** Caused by an Input/Output error which could not be identified. */ + public static final int ERROR_CODE_IO_UNSPECIFIED = 2000; + /** + * Caused by a network connection failure. + * + *

    The following is a non-exhaustive list of possible reasons: + * + *

      + *
    • There is no network connectivity. + *
    • The URL's domain is misspelled or does not exist. + *
    • The target host is unreachable. + *
    • The server unexpectedly closes the connection. + *
    + */ + public static final int ERROR_CODE_IO_NETWORK_CONNECTION_FAILED = 2001; + /** Caused by a network timeout, meaning the server is taking too long to fulfill a request. */ + public static final int ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT = 2002; + /** + * Caused by a server returning a resource with an invalid "Content-Type" HTTP header value. + * + *

    For example, this can happen when the player is expecting a piece of media, but the server + * returns a paywall HTML page, with content type "text/html". + */ + public static final int ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE = 2003; + /** Caused by an HTTP server returning an unexpected HTTP response status code. */ + public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 2004; + /** Caused by a non-existent file. */ + public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 2005; + /** + * Caused by lack of permission to perform an IO operation. For example, lack of permission to + * access internet or external storage. + */ + public static final int ERROR_CODE_IO_NO_PERMISSION = 2006; + /** + * Caused by the player trying to access cleartext HTTP traffic (meaning http:// rather than + * https://) when the app's Network Security Configuration does not permit it. + */ + public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 2007; + /** Caused by reading data out of the data bound. */ + public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 2008; + + // Decoding errors (3xxx). + + /** Caused by a decoder initialization failure. */ + public static final int ERROR_CODE_DECODER_INIT_FAILED = 3001; + /** Caused by a failure while trying to decode media samples. */ + public static final int ERROR_CODE_DECODING_FAILED = 3002; + /** Caused by trying to decode content whose format is not supported. */ + public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 3003; + + // Encoding errors (4xxx). + + /** Caused by an encoder initialization failure. */ + public static final int ERROR_CODE_ENCODER_INIT_FAILED = 4001; + /** Caused by a failure while trying to encode media samples. */ + public static final int ERROR_CODE_ENCODING_FAILED = 4002; + /** Caused by requesting to encode content in a format that is not supported by the device. */ + public static final int ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED = 4003; + + // Video editing errors (5xxx). + + /** Caused by a GL initialization failure. */ + public static final int ERROR_CODE_GL_INIT_FAILED = 5001; + /** Caused by a failure while using or releasing a GL program. */ + public static final int ERROR_CODE_GL_PROCESSING_FAILED = 5002; + + // Audio editing errors (6xxx). + + /** Caused by an audio processor initialization failure. */ + public static final int ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED = 6001; + + private static final ImmutableBiMap NAME_TO_ERROR_CODE = + new ImmutableBiMap.Builder() + .put("ERROR_CODE_FAILED_RUNTIME_CHECK", ERROR_CODE_FAILED_RUNTIME_CHECK) + .put("ERROR_CODE_IO_UNSPECIFIED", ERROR_CODE_IO_UNSPECIFIED) + .put("ERROR_CODE_IO_NETWORK_CONNECTION_FAILED", ERROR_CODE_IO_NETWORK_CONNECTION_FAILED) + .put("ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT", ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT) + .put("ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE", ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE) + .put("ERROR_CODE_IO_BAD_HTTP_STATUS", ERROR_CODE_IO_BAD_HTTP_STATUS) + .put("ERROR_CODE_IO_FILE_NOT_FOUND", ERROR_CODE_IO_FILE_NOT_FOUND) + .put("ERROR_CODE_IO_NO_PERMISSION", ERROR_CODE_IO_NO_PERMISSION) + .put("ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED", ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED) + .put("ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE", ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE) + .put("ERROR_CODE_DECODER_INIT_FAILED", ERROR_CODE_DECODER_INIT_FAILED) + .put("ERROR_CODE_DECODING_FAILED", ERROR_CODE_DECODING_FAILED) + .put("ERROR_CODE_DECODING_FORMAT_UNSUPPORTED", ERROR_CODE_DECODING_FORMAT_UNSUPPORTED) + .put("ERROR_CODE_ENCODER_INIT_FAILED", ERROR_CODE_ENCODER_INIT_FAILED) + .put("ERROR_CODE_ENCODING_FAILED", ERROR_CODE_ENCODING_FAILED) + .put("ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED", ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED) + .put("ERROR_CODE_GL_INIT_FAILED", ERROR_CODE_GL_INIT_FAILED) + .put("ERROR_CODE_GL_PROCESSING_FAILED", ERROR_CODE_GL_PROCESSING_FAILED) + .put("ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED", ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED) + .buildOrThrow(); + + /** Returns the {@code errorCode} for a given name. */ + private static @ErrorCode int getErrorCodeForName(String errorCodeName) { + return NAME_TO_ERROR_CODE.getOrDefault(errorCodeName, ERROR_CODE_UNSPECIFIED); + } + + /** Returns the name of a given {@code errorCode}. */ + public static String getErrorCodeName(@ErrorCode int errorCode) { + return NAME_TO_ERROR_CODE.inverse().getOrDefault(errorCode, "invalid error code"); + } + + /** + * Equivalent to {@link TransformationException#getErrorCodeName(int) + * TransformationException.getErrorCodeName(this.errorCode)}. + */ + public final String getErrorCodeName() { + return getErrorCodeName(errorCode); + } + /** * Creates an instance for a decoder or encoder related exception. * @@ -70,8 +233,8 @@ public final class TransformationException extends Exception { /** * Creates an instance for an unexpected exception. * - *

    If the exception is a runtime exception, error code {@link ERROR_CODE_FAILED_RUNTIME_CHECK} - * is used. Otherwise, the created instance has error code {@link ERROR_CODE_UNSPECIFIED}. + *

    If the exception is a runtime exception, error code {@link #ERROR_CODE_FAILED_RUNTIME_CHECK} + * is used. Otherwise, the created instance has error code {@link #ERROR_CODE_UNSPECIFIED}. * * @param cause The cause of the failure. * @return The created instance. @@ -85,107 +248,17 @@ public final class TransformationException extends Exception { } /** - * Codes that identify causes of {@link Transformer} errors. + * Converts a {@link PlaybackException} to a {@code TransformationException}. * - *

    This list of errors may be extended in future versions. + *

    If no corresponding error code exists, the created instance will have {@link + * #ERROR_CODE_UNSPECIFIED}. */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) - @IntDef( - open = true, - value = { - ERROR_CODE_UNSPECIFIED, - ERROR_CODE_FAILED_RUNTIME_CHECK, - ERROR_CODE_DECODER_INIT_FAILED, - ERROR_CODE_DECODING_FAILED, - ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, - ERROR_CODE_ENCODER_INIT_FAILED, - ERROR_CODE_ENCODING_FAILED, - ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED, - ERROR_CODE_GL_INIT_FAILED, - ERROR_CODE_GL_PROCESSING_FAILED, - ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED, - }) - public @interface ErrorCode {} - - // Miscellaneous errors (1xxx). - - /** Caused by an error whose cause could not be identified. */ - public static final int ERROR_CODE_UNSPECIFIED = 1000; - /** - * Caused by a failed runtime check. - * - *

    This can happen when transformer reaches an invalid state. - */ - public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 1001; - - // Decoding errors (2xxx). - - /** Caused by a decoder initialization failure. */ - public static final int ERROR_CODE_DECODER_INIT_FAILED = 2001; - /** Caused by a failure while trying to decode media samples. */ - public static final int ERROR_CODE_DECODING_FAILED = 2002; - /** Caused by trying to decode content whose format is not supported. */ - public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 2003; - - // Encoding errors (3xxx). - - /** Caused by an encoder initialization failure. */ - public static final int ERROR_CODE_ENCODER_INIT_FAILED = 3001; - /** Caused by a failure while trying to encode media samples. */ - public static final int ERROR_CODE_ENCODING_FAILED = 3002; - /** Caused by requesting to encode content in a format that is not supported by the device. */ - public static final int ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED = 3003; - - // Video editing errors (4xxx). - - /** Caused by a GL initialization failure. */ - public static final int ERROR_CODE_GL_INIT_FAILED = 4001; - /** Caused by a failure while using or releasing a GL program. */ - public static final int ERROR_CODE_GL_PROCESSING_FAILED = 4002; - - // Audio editing errors (5xxx). - - /** Caused by an audio processor initialization failure. */ - public static final int ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED = 5001; - - /** Returns the name of a given {@code errorCode}. */ - public static String getErrorCodeName(@ErrorCode int errorCode) { - switch (errorCode) { - case ERROR_CODE_UNSPECIFIED: - return "ERROR_CODE_UNSPECIFIED"; - case ERROR_CODE_FAILED_RUNTIME_CHECK: - return "ERROR_CODE_FAILED_RUNTIME_CHECK"; - case ERROR_CODE_DECODER_INIT_FAILED: - return "ERROR_CODE_DECODER_INIT_FAILED"; - case ERROR_CODE_DECODING_FAILED: - return "ERROR_CODE_DECODING_FAILED"; - case ERROR_CODE_DECODING_FORMAT_UNSUPPORTED: - return "ERROR_CODE_DECODING_FORMAT_UNSUPPORTED"; - case ERROR_CODE_ENCODER_INIT_FAILED: - return "ERROR_CODE_ENCODER_INIT_FAILED"; - case ERROR_CODE_ENCODING_FAILED: - return "ERROR_CODE_ENCODING_FAILED"; - case ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED: - return "ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED"; - case ERROR_CODE_GL_INIT_FAILED: - return "ERROR_CODE_GL_INIT_FAILED"; - case ERROR_CODE_GL_PROCESSING_FAILED: - return "ERROR_CODE_GL_PROCESSING_FAILED"; - case ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED: - return "ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED"; - default: - return "invalid error code"; - } - } - - /** - * Equivalent to {@link TransformationException#getErrorCodeName(int) - * TransformationException.getErrorCodeName(this.errorCode)}. - */ - public final String getErrorCodeName() { - return getErrorCodeName(errorCode); + /* package */ static TransformationException createForPlaybackException( + PlaybackException exception) { + return new TransformationException( + exception.getMessage(), + exception.getCause(), + getErrorCodeForName(exception.getErrorCodeName())); } /** An error code which identifies the cause of the transformation failure. */ diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java index b1688ee62b..b30299a359 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java @@ -845,7 +845,7 @@ public final class Transformer { handleTransformationEnded( cause instanceof TransformationException ? (TransformationException) cause - : TransformationException.createForUnexpected(error)); + : TransformationException.createForPlaybackException(error)); } private void handleTransformationEnded(@Nullable TransformationException exception) { diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java index 6bb8925bba..a72266330c 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java @@ -35,7 +35,6 @@ import android.view.Surface; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.testutil.DumpFileAsserts; import com.google.android.exoplayer2.testutil.FakeClock; @@ -295,15 +294,14 @@ public final class TransformerTest { } @Test - public void startTransformation_withPlayerError_completesWithError() throws Exception { + public void startTransformation_withIoError_completesWithError() throws Exception { Transformer transformer = new Transformer.Builder(context).setClock(clock).build(); MediaItem mediaItem = MediaItem.fromUri("asset:///non-existing-path.mp4"); transformer.startTransformation(mediaItem, outputPath); TransformationException exception = TransformerTestRunner.runUntilError(transformer); - assertThat(exception).hasCauseThat().isInstanceOf(ExoPlaybackException.class); - assertThat(exception).hasCauseThat().hasCauseThat().isInstanceOf(IOException.class); + assertThat(exception).hasCauseThat().isInstanceOf(IOException.class); } @Test From 3aae0cbff30693bbadbc4ae48fa59b027bb58f36 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 16 Dec 2021 14:20:14 +0000 Subject: [PATCH 25/56] Fix some comment references in StyledPlayer(Control)View These comments inadvertantly refer to types and drawables associated with Player(Control)View. PiperOrigin-RevId: 416794967 --- .../android/exoplayer2/ui/StyledPlayerControlView.java | 10 +++++----- .../google/android/exoplayer2/ui/StyledPlayerView.java | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index 3f42f19b71..b20adf781f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -241,17 +241,17 @@ import java.util.concurrent.CopyOnWriteArrayList; *

  • Type: {@link ImageView} *
  • Note: StyledPlayerControlView will programmatically set the drawable on the repeat * toggle button according to the player's current repeat mode. The drawables used are - * {@code exo_controls_repeat_off}, {@code exo_controls_repeat_one} and {@code - * exo_controls_repeat_all}. See the section above for information on overriding these - * drawables. + * {@code exo_styled_controls_repeat_off}, {@code exo_styled_controls_repeat_one} and + * {@code exo_styled_controls_repeat_all}. See the section above for information on + * overriding these drawables. * *
  • {@code exo_shuffle} - The shuffle button. *
      *
    • Type: {@link ImageView} *
    • Note: StyledPlayerControlView will programmatically set the drawable on the shuffle * button according to the player's current repeat mode. The drawables used are {@code - * exo_controls_shuffle_off} and {@code exo_controls_shuffle_on}. See the section above - * for information on overriding these drawables. + * exo_styled_controls_shuffle_off} and {@code exo_styled_controls_shuffle_on}. See the + * section above for information on overriding these drawables. *
    *
  • {@code exo_vr} - The VR mode button. *
      diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java index b8907094fd..4090e1f962 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java @@ -498,7 +498,7 @@ public class StyledPlayerView extends FrameLayout implements AdViewProvider { if (customController != null) { this.controller = customController; } else if (controllerPlaceholder != null) { - // Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are + // Propagate attrs as playbackAttrs so that StyledPlayerControlView's custom attributes are // transferred, but standard attributes (e.g. background) are not. this.controller = new StyledPlayerControlView(context, null, 0, attrs); controller.setId(R.id.exo_controller); From 1f69ab2d8ca0db9dc817e9633440b233169b4514 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 Dec 2021 14:24:25 +0000 Subject: [PATCH 26/56] Update `DEVSITE_TENANT` d.android.com location PiperOrigin-RevId: 416795667 --- javadoc_combined.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javadoc_combined.gradle b/javadoc_combined.gradle index 3336496a26..19d29ad2aa 100644 --- a/javadoc_combined.gradle +++ b/javadoc_combined.gradle @@ -109,7 +109,7 @@ class CombinedJavadocPlugin implements Plugin { "-loggingLevel", "WARN", "-sourceSet", "-src $sourcesString -classpath $dependenciesString", "-offlineMode") - environment("DEVSITE_TENANT", "androidx") + environment("DEVSITE_TENANT", "androidx/media3") } description = "Generates combined javadoc for developer.android.com." classpath = project.files(new File(getTemporaryDir(), "dackka.jar")) From 3f5304b8915ae75ebb2e1bfa89c48481a4f7995e Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 16 Dec 2021 15:45:37 +0000 Subject: [PATCH 27/56] Fix StyledPlayerControlView reference in demo app's PlayerActivity PiperOrigin-RevId: 416809105 --- .../java/com/google/android/exoplayer2/demo/PlayerActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 302138d947..9a19edf804 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -234,7 +234,7 @@ public class PlayerActivity extends AppCompatActivity } } - // PlayerControlView.VisibilityListener implementation + // StyledPlayerControlView.VisibilityListener implementation @Override public void onVisibilityChange(int visibility) { From c1f878deb1ffda388046636c4839852610c3e186 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 16 Dec 2021 16:24:32 +0000 Subject: [PATCH 28/56] Move DefaultMediaSourceFactory.AdsLoaderProvider to AdsLoader.Provider Keep the old interface deprecated so any app code implementing it by name (rather than with a lambda) will continue to work. PiperOrigin-RevId: 416816566 --- docs/ad-insertion.md | 4 +-- .../source/DefaultMediaSourceFactory.java | 28 +++++-------------- .../exoplayer2/source/ads/AdsLoader.java | 20 +++++++++++++ 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/docs/ad-insertion.md b/docs/ad-insertion.md index 4e42430f5b..090d0e49d9 100644 --- a/docs/ad-insertion.md +++ b/docs/ad-insertion.md @@ -46,7 +46,7 @@ MediaItem mediaItem = To enable player support for media items that specify ad tags, it's necessary to build and inject a `DefaultMediaSourceFactory` configured with an -`AdsLoaderProvider` and an `AdViewProvider` when creating the player: +`AdsLoader.Provider` and an `AdViewProvider` when creating the player: ~~~ MediaSourceFactory mediaSourceFactory = @@ -61,7 +61,7 @@ ExoPlayer player = new ExoPlayer.Builder(context) Internally, `DefaultMediaSourceFactory` will wrap the content media source in an `AdsMediaSource`. The `AdsMediaSource` will obtain an `AdsLoader` from the -`AdsLoaderProvider` and use it to insert ads as defined by the media item's ad +`AdsLoader.Provider` and use it to insert ads as defined by the media item's ad tag. ExoPlayer's `StyledPlayerView` and `PlayerView` UI components both implement diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java index a34f1023a0..38bcb12391 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java @@ -97,23 +97,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ public final class DefaultMediaSourceFactory implements MediaSourceFactory { - /** - * Provides {@link AdsLoader} instances for media items that have {@link - * MediaItem.LocalConfiguration#adsConfiguration ad tag URIs}. - */ - public interface AdsLoaderProvider { - - /** - * Returns an {@link AdsLoader} for the given {@link - * MediaItem.LocalConfiguration#adsConfiguration ads configuration}, or {@code null} if no ads - * loader is available for the given ads configuration. - * - *

      This method is called each time a {@link MediaSource} is created from a {@link MediaItem} - * that defines an {@link MediaItem.LocalConfiguration#adsConfiguration ads configuration}. - */ - @Nullable - AdsLoader getAdsLoader(MediaItem.AdsConfiguration adsConfiguration); - } + /** @deprecated Use {@link AdsLoader.Provider} instead. */ + @Deprecated + public interface AdsLoaderProvider extends AdsLoader.Provider {} private static final String TAG = "DMediaSourceFactory"; @@ -121,7 +107,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { private final DelegateFactoryLoader delegateFactoryLoader; @Nullable private final MediaSourceFactory serverSideDaiMediaSourceFactory; - @Nullable private AdsLoaderProvider adsLoaderProvider; + @Nullable private AdsLoader.Provider adsLoaderProvider; @Nullable private AdViewProvider adViewProvider; @Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long liveTargetOffsetMs; @@ -211,14 +197,14 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { } /** - * Sets the {@link AdsLoaderProvider} that provides {@link AdsLoader} instances for media items + * Sets the {@link AdsLoader.Provider} that provides {@link AdsLoader} instances for media items * that have {@link MediaItem.LocalConfiguration#adsConfiguration ads configurations}. * * @param adsLoaderProvider A provider for {@link AdsLoader} instances. * @return This factory, for convenience. */ public DefaultMediaSourceFactory setAdsLoaderProvider( - @Nullable AdsLoaderProvider adsLoaderProvider) { + @Nullable AdsLoader.Provider adsLoaderProvider) { this.adsLoaderProvider = adsLoaderProvider; return this; } @@ -457,7 +443,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { if (adsConfiguration == null) { return mediaSource; } - @Nullable AdsLoaderProvider adsLoaderProvider = this.adsLoaderProvider; + @Nullable AdsLoader.Provider adsLoaderProvider = this.adsLoaderProvider; @Nullable AdViewProvider adViewProvider = this.adViewProvider; if (adsLoaderProvider == null || adViewProvider == null) { Log.w( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java index f937b0a07b..c28131aacf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -17,7 +17,9 @@ package com.google.android.exoplayer2.source.ads; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; import com.google.android.exoplayer2.ui.AdViewProvider; import com.google.android.exoplayer2.upstream.DataSpec; @@ -45,6 +47,24 @@ import java.io.IOException; */ public interface AdsLoader { + /** + * Provides {@link AdsLoader} instances for media items that have {@link + * MediaItem.LocalConfiguration#adsConfiguration ad tag URIs}. + */ + interface Provider { + + /** + * Returns an {@link AdsLoader} for the given {@link + * MediaItem.LocalConfiguration#adsConfiguration ads configuration}, or {@code null} if no ads + * loader is available for the given ads configuration. + * + *

      This method is called each time a {@link MediaSource} is created from a {@link MediaItem} + * that defines an {@link MediaItem.LocalConfiguration#adsConfiguration ads configuration}. + */ + @Nullable + AdsLoader getAdsLoader(MediaItem.AdsConfiguration adsConfiguration); + } + /** Listener for ads loader events. All methods are called on the main thread. */ interface EventListener { From 6cce3dfb50ec29055c8a6d39774b228e98a46edd Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 16 Dec 2021 17:20:35 +0000 Subject: [PATCH 29/56] Remove references to `MediaSourceFactory#setStreamKeys` from dev guide This method has been deprecated since 2.12.0: https://github.com/google/ExoPlayer/commit/d1bbd3507a818e14be965c300938f9d51f8b7836 PiperOrigin-RevId: 416827149 --- docs/downloading-media.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/downloading-media.md b/docs/downloading-media.md index 6bef8a25d6..a835644d85 100644 --- a/docs/downloading-media.md +++ b/docs/downloading-media.md @@ -394,9 +394,7 @@ When building the `MediaItem`, `MediaItem.playbackProperties.streamKeys` must be set to match those in the `DownloadRequest` so that the player only tries to play the subset of tracks that have been downloaded. Using `Download.request.toMediaItem` and `DownloadRequest.toMediaItem` to build the -`MediaItem` will take care of this for you. If building a `MediaSource` to pass -directly to the player, it is similarly important to configure the stream keys -by calling `MediaSourceFactory.setStreamKeys`. +`MediaItem` will take care of this for you. If you see data being requested from the network when trying to play downloaded adaptive content, the most likely cause is that the player is trying to adapt to From e5ec6b31b239319a1076df9ab1a7a6b7ce3ac9dc Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 16 Dec 2021 18:03:40 +0000 Subject: [PATCH 30/56] Store adPlaybackStates and shared periods by period UID To support multi-period content we need to store AdPlaybackStates and SharedMediaPeriod by the periodUid as a key. While after this no-op CL, we still only support single-period content, storing these resources by periodUid is the ground work for multi-period support being added in an follow-up CL. PiperOrigin-RevId: 416836445 --- .../ads/ServerSideAdInsertionMediaSource.java | 175 ++++++++++++------ .../ServerSideAdInsertionMediaSourceTest.java | 151 +++++++++++---- 2 files changed, 232 insertions(+), 94 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSource.java index 5870fb1872..c663b1aa3d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSource.java @@ -22,6 +22,7 @@ import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.getStreamPositionUs; import static com.google.android.exoplayer2.util.Assertions.checkArgument; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; import static com.google.android.exoplayer2.util.Util.castNonNull; import android.os.Handler; @@ -52,9 +53,9 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import java.io.IOException; @@ -73,8 +74,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * server-side inserted ad breaks and ensures that playback continues seamlessly with the wrapped * media across all transitions. * - *

      The ad breaks need to be specified using {@link #setAdPlaybackState} and can be updated during - * playback. + *

      The ad breaks need to be specified using {@link #setAdPlaybackStates} and can be updated + * during playback. */ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource implements MediaSource.MediaSourceCaller, MediaSourceEventListener, DrmSessionEventListener { @@ -88,7 +89,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource * Called when the content source has refreshed the timeline. * *

      If true is returned the source refresh publication is deferred, to wait for an {@link - * #setAdPlaybackState(AdPlaybackState) ad playback state update}. If false is returned, the + * #setAdPlaybackStates(ImmutableMap)} ad playback state update}. If false is returned, the * source refresh is immediately published. * *

      Called on the playback thread. @@ -101,7 +102,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource } private final MediaSource mediaSource; - private final ListMultimap mediaPeriods; + private final ListMultimap, SharedMediaPeriod> mediaPeriods; private final MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcherWithoutId; private final DrmSessionEventListener.EventDispatcher drmEventDispatcherWithoutId; @Nullable private final AdPlaybackStateUpdater adPlaybackStateUpdater; @@ -112,7 +113,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource @Nullable private SharedMediaPeriod lastUsedMediaPeriod; @Nullable private Timeline contentTimeline; - private AdPlaybackState adPlaybackState; + private ImmutableMap adPlaybackStates; /** * Creates the media source. @@ -128,53 +129,74 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource this.mediaSource = mediaSource; this.adPlaybackStateUpdater = adPlaybackStateUpdater; mediaPeriods = ArrayListMultimap.create(); - adPlaybackState = AdPlaybackState.NONE; + adPlaybackStates = ImmutableMap.of(); mediaSourceEventDispatcherWithoutId = createEventDispatcher(/* mediaPeriodId= */ null); drmEventDispatcherWithoutId = createDrmEventDispatcher(/* mediaPeriodId= */ null); } /** - * Sets the {@link AdPlaybackState} published by this source. + * Sets the map of {@link AdPlaybackState ad playback states} published by this source. The key is + * the period UID of a period in the {@link + * AdPlaybackStateUpdater#onAdPlaybackStateUpdateRequested(Timeline)} content timeline}. + * + *

      Each period has an {@link AdPlaybackState} that tells where in the period the ad groups + * start and end. Must only contain server-side inserted ad groups. The number of ad groups and + * the number of ads within an ad group may only increase. The durations of ads may change and the + * positions of future ad groups may change. Post-roll ad groups with {@link C#TIME_END_OF_SOURCE} + * must be empty and can be used as a placeholder for a future ad group. * *

      May be called from any thread. * - *

      Must only contain server-side inserted ad groups. The number of ad groups and the number of - * ads within an ad group may only increase. The durations of ads may change and the positions of - * future ad groups may change. Post-roll ad groups with {@link C#TIME_END_OF_SOURCE} must be - * empty and can be used as a placeholder for a future ad group. - * - * @param adPlaybackState The new {@link AdPlaybackState}. + * @param adPlaybackStates The map of {@link AdPlaybackState} keyed by their period UID. */ - public void setAdPlaybackState(AdPlaybackState adPlaybackState) { - checkArgument(adPlaybackState.adGroupCount >= this.adPlaybackState.adGroupCount); - for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) { - AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(i); - checkArgument(adGroup.isServerSideInserted); - if (i < this.adPlaybackState.adGroupCount) { - checkArgument( - getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ i) - >= getAdCountInGroup(this.adPlaybackState, /* adGroupIndex= */ i)); - } - if (adGroup.timeUs == C.TIME_END_OF_SOURCE) { - checkArgument(getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ i) == 0); + public void setAdPlaybackStates(ImmutableMap adPlaybackStates) { + checkArgument(!adPlaybackStates.isEmpty()); + Object adsId = checkNotNull(adPlaybackStates.values().asList().get(0).adsId); + for (Map.Entry entry : adPlaybackStates.entrySet()) { + Object periodUid = entry.getKey(); + AdPlaybackState adPlaybackState = entry.getValue(); + checkArgument(Util.areEqual(adsId, adPlaybackState.adsId)); + @Nullable AdPlaybackState oldAdPlaybackState = this.adPlaybackStates.get(periodUid); + if (oldAdPlaybackState != null) { + for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) { + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(i); + checkArgument(adGroup.isServerSideInserted); + if (i < oldAdPlaybackState.adGroupCount) { + checkArgument( + getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ i) + >= getAdCountInGroup(oldAdPlaybackState, /* adGroupIndex= */ i)); + } + if (adGroup.timeUs == C.TIME_END_OF_SOURCE) { + checkArgument(getAdCountInGroup(adPlaybackState, /* adGroupIndex= */ i) == 0); + } + } } } synchronized (this) { if (playbackHandler == null) { - this.adPlaybackState = adPlaybackState; + this.adPlaybackStates = adPlaybackStates; } else { playbackHandler.post( () -> { for (SharedMediaPeriod mediaPeriod : mediaPeriods.values()) { - mediaPeriod.updateAdPlaybackState(adPlaybackState); + @Nullable + AdPlaybackState adPlaybackState = adPlaybackStates.get(mediaPeriod.periodUid); + if (adPlaybackState != null) { + mediaPeriod.updateAdPlaybackState(adPlaybackState); + } } if (lastUsedMediaPeriod != null) { - lastUsedMediaPeriod.updateAdPlaybackState(adPlaybackState); + @Nullable + AdPlaybackState adPlaybackState = + adPlaybackStates.get(lastUsedMediaPeriod.periodUid); + if (adPlaybackState != null) { + lastUsedMediaPeriod.updateAdPlaybackState(adPlaybackState); + } } - this.adPlaybackState = adPlaybackState; + this.adPlaybackStates = adPlaybackStates; if (contentTimeline != null) { refreshSourceInfo( - new ServerSideAdInsertionTimeline(contentTimeline, adPlaybackState)); + new ServerSideAdInsertionTimeline(contentTimeline, adPlaybackStates)); } }); } @@ -218,8 +240,8 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource this.contentTimeline = timeline; if ((adPlaybackStateUpdater == null || !adPlaybackStateUpdater.onAdPlaybackStateUpdateRequested(timeline)) - && !AdPlaybackState.NONE.equals(adPlaybackState)) { - refreshSourceInfo(new ServerSideAdInsertionTimeline(timeline, adPlaybackState)); + && !adPlaybackStates.isEmpty()) { + refreshSourceInfo(new ServerSideAdInsertionTimeline(timeline, adPlaybackStates)); } } @@ -237,19 +259,26 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { - SharedMediaPeriod sharedPeriod; + @Nullable SharedMediaPeriod sharedPeriod = null; + Pair sharedMediaPeriodKey = new Pair<>(id.windowSequenceNumber, id.periodUid); if (lastUsedMediaPeriod != null) { - sharedPeriod = lastUsedMediaPeriod; + if (lastUsedMediaPeriod.periodUid.equals(id.periodUid)) { + sharedPeriod = lastUsedMediaPeriod; + mediaPeriods.put(sharedMediaPeriodKey, sharedPeriod); + } else { + lastUsedMediaPeriod.release(mediaSource); + } lastUsedMediaPeriod = null; - mediaPeriods.put(id.windowSequenceNumber, sharedPeriod); - } else { + } + if (sharedPeriod == null) { @Nullable SharedMediaPeriod lastExistingPeriod = - Iterables.getLast(mediaPeriods.get(id.windowSequenceNumber), /* defaultValue= */ null); + Iterables.getLast(mediaPeriods.get(sharedMediaPeriodKey), /* defaultValue= */ null); if (lastExistingPeriod != null && lastExistingPeriod.canReuseMediaPeriod(id, startPositionUs)) { sharedPeriod = lastExistingPeriod; } else { + AdPlaybackState adPlaybackState = checkNotNull(adPlaybackStates.get(id.periodUid)); long streamPositionUs = getStreamPositionUs(startPositionUs, id, adPlaybackState); sharedPeriod = new SharedMediaPeriod( @@ -257,8 +286,9 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource new MediaPeriodId(id.periodUid, id.windowSequenceNumber), allocator, streamPositionUs), + id.periodUid, adPlaybackState); - mediaPeriods.put(id.windowSequenceNumber, sharedPeriod); + mediaPeriods.put(sharedMediaPeriodKey, sharedPeriod); } } MediaPeriodImpl mediaPeriod = @@ -274,7 +304,10 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource mediaPeriodImpl.sharedPeriod.remove(mediaPeriodImpl); if (mediaPeriodImpl.sharedPeriod.isUnused()) { mediaPeriods.remove( - mediaPeriodImpl.mediaPeriodId.windowSequenceNumber, mediaPeriodImpl.sharedPeriod); + new Pair<>( + mediaPeriodImpl.mediaPeriodId.windowSequenceNumber, + mediaPeriodImpl.mediaPeriodId.periodUid), + mediaPeriodImpl.sharedPeriod); if (mediaPeriods.isEmpty()) { // Keep until disabled. lastUsedMediaPeriod = mediaPeriodImpl.sharedPeriod; @@ -378,7 +411,11 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource } else { mediaPeriod.sharedPeriod.onLoadStarted(loadEventInfo, mediaLoadData); mediaPeriod.mediaSourceEventDispatcher.loadStarted( - loadEventInfo, correctMediaLoadData(mediaPeriod, mediaLoadData, adPlaybackState)); + loadEventInfo, + correctMediaLoadData( + mediaPeriod, + mediaLoadData, + checkNotNull(adPlaybackStates.get(mediaPeriod.mediaPeriodId.periodUid)))); } } @@ -396,7 +433,11 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource } else { mediaPeriod.sharedPeriod.onLoadFinished(loadEventInfo); mediaPeriod.mediaSourceEventDispatcher.loadCompleted( - loadEventInfo, correctMediaLoadData(mediaPeriod, mediaLoadData, adPlaybackState)); + loadEventInfo, + correctMediaLoadData( + mediaPeriod, + mediaLoadData, + checkNotNull(adPlaybackStates.get(mediaPeriod.mediaPeriodId.periodUid)))); } } @@ -414,7 +455,11 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource } else { mediaPeriod.sharedPeriod.onLoadFinished(loadEventInfo); mediaPeriod.mediaSourceEventDispatcher.loadCanceled( - loadEventInfo, correctMediaLoadData(mediaPeriod, mediaLoadData, adPlaybackState)); + loadEventInfo, + correctMediaLoadData( + mediaPeriod, + mediaLoadData, + checkNotNull(adPlaybackStates.get(mediaPeriod.mediaPeriodId.periodUid)))); } } @@ -438,7 +483,10 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource } mediaPeriod.mediaSourceEventDispatcher.loadError( loadEventInfo, - correctMediaLoadData(mediaPeriod, mediaLoadData, adPlaybackState), + correctMediaLoadData( + mediaPeriod, + mediaLoadData, + checkNotNull(adPlaybackStates.get(mediaPeriod.mediaPeriodId.periodUid))), error, wasCanceled); } @@ -454,7 +502,10 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource mediaSourceEventDispatcherWithoutId.upstreamDiscarded(mediaLoadData); } else { mediaPeriod.mediaSourceEventDispatcher.upstreamDiscarded( - correctMediaLoadData(mediaPeriod, mediaLoadData, adPlaybackState)); + correctMediaLoadData( + mediaPeriod, + mediaLoadData, + checkNotNull(adPlaybackStates.get(mediaPeriod.mediaPeriodId.periodUid)))); } } @@ -469,7 +520,10 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource } else { mediaPeriod.sharedPeriod.onDownstreamFormatChanged(mediaPeriod, mediaLoadData); mediaPeriod.mediaSourceEventDispatcher.downstreamFormatChanged( - correctMediaLoadData(mediaPeriod, mediaLoadData, adPlaybackState)); + correctMediaLoadData( + mediaPeriod, + mediaLoadData, + checkNotNull(adPlaybackStates.get(mediaPeriod.mediaPeriodId.periodUid)))); } } @@ -488,7 +542,8 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource if (mediaPeriodId == null) { return null; } - List periods = mediaPeriods.get(mediaPeriodId.windowSequenceNumber); + List periods = + mediaPeriods.get(new Pair<>(mediaPeriodId.windowSequenceNumber, mediaPeriodId.periodUid)); if (periods.isEmpty()) { return null; } @@ -557,6 +612,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource private final MediaPeriod actualMediaPeriod; private final List mediaPeriods; private final Map> activeLoads; + private final Object periodUid; private AdPlaybackState adPlaybackState; @Nullable private MediaPeriodImpl loadingPeriod; @@ -566,8 +622,10 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource public @NullableType SampleStream[] sampleStreams; public @NullableType MediaLoadData[] lastDownstreamFormatChangeData; - public SharedMediaPeriod(MediaPeriod actualMediaPeriod, AdPlaybackState adPlaybackState) { + public SharedMediaPeriod( + MediaPeriod actualMediaPeriod, Object periodUid, AdPlaybackState adPlaybackState) { this.actualMediaPeriod = actualMediaPeriod; + this.periodUid = periodUid; this.adPlaybackState = adPlaybackState; mediaPeriods = new ArrayList<>(); activeLoads = new HashMap<>(); @@ -928,19 +986,27 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource private static final class ServerSideAdInsertionTimeline extends ForwardingTimeline { - private final AdPlaybackState adPlaybackState; + private final ImmutableMap adPlaybackStates; public ServerSideAdInsertionTimeline( - Timeline contentTimeline, AdPlaybackState adPlaybackState) { + Timeline contentTimeline, ImmutableMap adPlaybackStates) { super(contentTimeline); - Assertions.checkState(contentTimeline.getPeriodCount() == 1); - Assertions.checkState(contentTimeline.getWindowCount() == 1); - this.adPlaybackState = adPlaybackState; + checkState(contentTimeline.getPeriodCount() == 1); + checkState(contentTimeline.getWindowCount() == 1); + Period period = new Period(); + for (int i = 0; i < contentTimeline.getPeriodCount(); i++) { + contentTimeline.getPeriod(/* periodIndex= */ i, period, /* setIds= */ true); + checkState(adPlaybackStates.containsKey(checkNotNull(period.uid))); + } + this.adPlaybackStates = adPlaybackStates; } @Override public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { super.getWindow(windowIndex, window, defaultPositionProjectionUs); + Object firstPeriodUid = + checkNotNull(getPeriod(/* periodIndex= */ 0, new Period(), /* setIds= */ true).uid); + AdPlaybackState adPlaybackState = checkNotNull(adPlaybackStates.get(firstPeriodUid)); long positionInPeriodUs = getMediaPeriodPositionUsForContent( window.positionInFirstPeriodUs, @@ -965,7 +1031,8 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { - super.getPeriod(periodIndex, period, setIds); + super.getPeriod(periodIndex, period, /* setIds= */ true); + AdPlaybackState adPlaybackState = checkNotNull(adPlaybackStates.get(period.uid)); long durationUs = period.durationUs; if (durationUs == C.TIME_UNSET) { durationUs = adPlaybackState.contentDurationUs; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSourceTest.java index 8d662bb445..66d85e360f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideAdInsertionMediaSourceTest.java @@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.play import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState; import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -32,6 +33,7 @@ import static org.mockito.Mockito.verify; import android.content.Context; import android.graphics.SurfaceTexture; +import android.util.Pair; import android.view.Surface; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -49,7 +51,9 @@ import com.google.android.exoplayer2.testutil.DumpFileAsserts; import com.google.android.exoplayer2.testutil.FakeClock; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.common.collect.ImmutableMap; import java.util.concurrent.atomic.AtomicReference; +import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -103,8 +107,8 @@ public final class ServerSideAdInsertionMediaSourceTest { .withContentResumeOffsetUs(/* adGroupIndex= */ 1, /* contentResumeOffsetUs= */ 400_000) .withContentResumeOffsetUs(/* adGroupIndex= */ 2, /* contentResumeOffsetUs= */ 200_000); AtomicReference timelineReference = new AtomicReference<>(); + mediaSource.setAdPlaybackStates(ImmutableMap.of(new Pair<>(0, 0), adPlaybackState)); - mediaSource.setAdPlaybackState(adPlaybackState); mediaSource.prepareSource( (source, timeline) -> timelineReference.set(timeline), /* mediaTransferListener= */ null, @@ -142,6 +146,26 @@ public final class ServerSideAdInsertionMediaSourceTest { assertThat(window.durationUs).isEqualTo(9_800_000); } + @Test + public void timeline_missingAdPlaybackStateByPeriodUid_isAssertedAndThrows() { + ServerSideAdInsertionMediaSource mediaSource = + new ServerSideAdInsertionMediaSource( + new FakeMediaSource(), /* adPlaybackStateUpdater= */ null); + // The map of adPlaybackStates does not contain a valid period UID as key. + mediaSource.setAdPlaybackStates( + ImmutableMap.of(new Object(), new AdPlaybackState(/* adsId= */ new Object()))); + + Assert.assertThrows( + IllegalStateException.class, + () -> + mediaSource.prepareSource( + (source, timeline) -> { + /* Do nothing. */ + }, + /* mediaTransferListener= */ null, + PlayerId.UNSET)); + } + @Test public void playbackWithPredefinedAds_playsSuccessfulWithoutRendererResets() throws Exception { Context context = ApplicationProvider.getApplicationContext(); @@ -153,10 +177,6 @@ public final class ServerSideAdInsertionMediaSourceTest { player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory); - ServerSideAdInsertionMediaSource mediaSource = - new ServerSideAdInsertionMediaSource( - new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), - /* adPlaybackStateUpdater= */ null); AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object()); adPlaybackState = addAdGroupToAdPlaybackState( @@ -170,17 +190,32 @@ public final class ServerSideAdInsertionMediaSourceTest { /* fromPositionUs= */ 400_000, /* toPositionUs= */ 700_000, /* contentResumeOffsetUs= */ 1_000_000); - adPlaybackState = + AdPlaybackState firstAdPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, /* fromPositionUs= */ 900_000, /* toPositionUs= */ 1_000_000, /* contentResumeOffsetUs= */ 0); - mediaSource.setAdPlaybackState(adPlaybackState); + + AtomicReference mediaSourceRef = new AtomicReference<>(); + mediaSourceRef.set( + new ServerSideAdInsertionMediaSource( + new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), + contentTimeline -> { + Object periodUid = + checkNotNull( + contentTimeline.getPeriod( + /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) + .uid); + mediaSourceRef + .get() + .setAdPlaybackStates(ImmutableMap.of(periodUid, firstAdPlaybackState)); + return true; + })); AnalyticsListener listener = mock(AnalyticsListener.class); player.addAnalyticsListener(listener); - player.setMediaSource(mediaSource); + player.setMediaSource(mediaSourceRef.get()); player.prepare(); player.play(); runUntilPlaybackState(player, Player.STATE_ENDED); @@ -204,6 +239,7 @@ public final class ServerSideAdInsertionMediaSourceTest { @Test public void playbackWithNewlyInsertedAds_playsSuccessfulWithoutRendererResets() throws Exception { Context context = ApplicationProvider.getApplicationContext(); + AtomicReference periodUid = new AtomicReference<>(); CapturingRenderersFactory renderersFactory = new CapturingRenderersFactory(context); ExoPlayer player = new ExoPlayer.Builder(context, renderersFactory) @@ -212,33 +248,43 @@ public final class ServerSideAdInsertionMediaSourceTest { player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory); - ServerSideAdInsertionMediaSource mediaSource = - new ServerSideAdInsertionMediaSource( - new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), - /* adPlaybackStateUpdater= */ null); - AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object()); - adPlaybackState = + AdPlaybackState firstAdPlaybackState = addAdGroupToAdPlaybackState( - adPlaybackState, + new AdPlaybackState(/* adsId= */ new Object()), /* fromPositionUs= */ 900_000, /* toPositionUs= */ 1_000_000, /* contentResumeOffsetUs= */ 0); - mediaSource.setAdPlaybackState(adPlaybackState); - + AtomicReference mediaSourceRef = new AtomicReference<>(); + mediaSourceRef.set( + new ServerSideAdInsertionMediaSource( + new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), + /* adPlaybackStateUpdater= */ contentTimeline -> { + periodUid.set( + checkNotNull( + contentTimeline.getPeriod( + /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) + .uid)); + mediaSourceRef + .get() + .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), firstAdPlaybackState)); + return true; + })); AnalyticsListener listener = mock(AnalyticsListener.class); player.addAnalyticsListener(listener); - player.setMediaSource(mediaSource); + player.setMediaSource(mediaSourceRef.get()); player.prepare(); // Add ad at the current playback position during playback. runUntilPlaybackState(player, Player.STATE_READY); - adPlaybackState = + AdPlaybackState secondAdPlaybackState = addAdGroupToAdPlaybackState( - adPlaybackState, + firstAdPlaybackState, /* fromPositionUs= */ 0, /* toPositionUs= */ 500_000, /* contentResumeOffsetUs= */ 0); - mediaSource.setAdPlaybackState(adPlaybackState); + mediaSourceRef + .get() + .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState)); runUntilPendingCommandsAreFullyHandled(player); player.play(); @@ -264,6 +310,7 @@ public final class ServerSideAdInsertionMediaSourceTest { public void playbackWithAdditionalAdsInAdGroup_playsSuccessfulWithoutRendererResets() throws Exception { Context context = ApplicationProvider.getApplicationContext(); + AtomicReference periodUid = new AtomicReference<>(); CapturingRenderersFactory renderersFactory = new CapturingRenderersFactory(context); ExoPlayer player = new ExoPlayer.Builder(context, renderersFactory) @@ -272,32 +319,45 @@ public final class ServerSideAdInsertionMediaSourceTest { player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory); - ServerSideAdInsertionMediaSource mediaSource = - new ServerSideAdInsertionMediaSource( - new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), - /* adPlaybackStateUpdater= */ null); - AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object()); - adPlaybackState = + AdPlaybackState firstAdPlaybackState = addAdGroupToAdPlaybackState( - adPlaybackState, + new AdPlaybackState(/* adsId= */ new Object()), /* fromPositionUs= */ 0, /* toPositionUs= */ 500_000, /* contentResumeOffsetUs= */ 0); - mediaSource.setAdPlaybackState(adPlaybackState); + AtomicReference mediaSourceRef = new AtomicReference<>(); + mediaSourceRef.set( + new ServerSideAdInsertionMediaSource( + new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), + /* adPlaybackStateUpdater= */ contentTimeline -> { + if (periodUid.get() == null) { + periodUid.set( + checkNotNull( + contentTimeline.getPeriod( + /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) + .uid)); + mediaSourceRef + .get() + .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), firstAdPlaybackState)); + } + return true; + })); AnalyticsListener listener = mock(AnalyticsListener.class); player.addAnalyticsListener(listener); - player.setMediaSource(mediaSource); + player.setMediaSource(mediaSourceRef.get()); player.prepare(); // Wait until playback is ready with first ad and then replace by 3 ads. runUntilPlaybackState(player, Player.STATE_READY); - adPlaybackState = - adPlaybackState + AdPlaybackState secondAdPlaybackState = + firstAdPlaybackState .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3) .withAdDurationsUs( /* adGroupIndex= */ 0, /* adDurationsUs...= */ 50_000, 250_000, 200_000); - mediaSource.setAdPlaybackState(adPlaybackState); + mediaSourceRef + .get() + .setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState)); runUntilPendingCommandsAreFullyHandled(player); player.play(); @@ -326,10 +386,6 @@ public final class ServerSideAdInsertionMediaSourceTest { new ExoPlayer.Builder(context).setClock(new FakeClock(/* isAutoAdvancing= */ true)).build(); player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); - ServerSideAdInsertionMediaSource mediaSource = - new ServerSideAdInsertionMediaSource( - new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), - /* adPlaybackStateUpdater= */ null); AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object()); adPlaybackState = addAdGroupToAdPlaybackState( @@ -343,17 +399,32 @@ public final class ServerSideAdInsertionMediaSourceTest { /* fromPositionUs= */ 600_000, /* toPositionUs= */ 700_000, /* contentResumeOffsetUs= */ 1_000_000); - adPlaybackState = + AdPlaybackState firstAdPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, /* fromPositionUs= */ 900_000, /* toPositionUs= */ 1_000_000, /* contentResumeOffsetUs= */ 0); - mediaSource.setAdPlaybackState(adPlaybackState); + + AtomicReference mediaSourceRef = new AtomicReference<>(); + mediaSourceRef.set( + new ServerSideAdInsertionMediaSource( + new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(TEST_ASSET)), + /* adPlaybackStateUpdater= */ contentTimeline -> { + Object periodUid = + checkNotNull( + contentTimeline.getPeriod( + /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) + .uid); + mediaSourceRef + .get() + .setAdPlaybackStates(ImmutableMap.of(periodUid, firstAdPlaybackState)); + return true; + })); AnalyticsListener listener = mock(AnalyticsListener.class); player.addAnalyticsListener(listener); - player.setMediaSource(mediaSource); + player.setMediaSource(mediaSourceRef.get()); player.prepare(); // Play to the first content part, then seek past the midroll. playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 100); From 7cd1a1d5de16f259f9fc24fb4b30aed853f72464 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 Dec 2021 18:51:34 +0000 Subject: [PATCH 31/56] Include images in dackka output PiperOrigin-RevId: 416848472 --- javadoc_combined.gradle | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/javadoc_combined.gradle b/javadoc_combined.gradle index 19d29ad2aa..a121169339 100644 --- a/javadoc_combined.gradle +++ b/javadoc_combined.gradle @@ -67,12 +67,12 @@ class CombinedJavadocPlugin implements Plugin { } } + def dackkaOutputDir = project.file("$project.buildDir/docs/dackka") project.task(DACKKA_TASK_NAME, type: JavaExec) { doFirst { // Recreate the output directory to remove any leftover files from a previous run. - def outputDir = project.file("$project.buildDir/docs/dackka") - project.delete outputDir - project.mkdir outputDir + project.delete dackkaOutputDir + project.mkdir dackkaOutputDir // Download the Dackka JAR. new URL(DACKKA_JAR_URL).withInputStream { @@ -104,7 +104,7 @@ class CombinedJavadocPlugin implements Plugin { .filter({ f -> project.file(f).exists() }).join(";") def dependenciesString = project.files(dependencies).asPath.replace(':', ';') args("-moduleName", "", - "-outputDir", "$outputDir", + "-outputDir", "$dackkaOutputDir", "-globalLinks", "$globalLinksString", "-loggingLevel", "WARN", "-sourceSet", "-src $sourcesString -classpath $dependenciesString", @@ -113,6 +113,18 @@ class CombinedJavadocPlugin implements Plugin { } description = "Generates combined javadoc for developer.android.com." classpath = project.files(new File(getTemporaryDir(), "dackka.jar")) + doLast { + libraryModules.each { libraryModule -> + project.copy { + from "${libraryModule.projectDir}/src/main/javadoc" + into "${dackkaOutputDir}/reference/" + } + project.copy { + from "${libraryModule.projectDir}/src/main/javadoc" + into "${dackkaOutputDir}/reference/kotlin/" + } + } + } } } } From 86ca5b8ec1e9109b9c1a2d774f4a385e9987c17a Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 20 Dec 2021 12:20:32 +0000 Subject: [PATCH 32/56] Add AdOverlayInfo.Builder and tweak @Purpose IntDef definition PiperOrigin-RevId: 417378468 --- .../android/exoplayer2/ui/AdOverlayInfo.java | 72 ++++++++++++++----- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ui/AdOverlayInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ui/AdOverlayInfo.java index b3eee8970a..ef58b8190d 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ui/AdOverlayInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ui/AdOverlayInfo.java @@ -15,12 +15,19 @@ */ package com.google.android.exoplayer2.ui; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; + import android.view.View; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** Provides information about an overlay view shown on top of an ad view group. */ public final class AdOverlayInfo { @@ -31,41 +38,70 @@ public final class AdOverlayInfo { */ @Documented @Retention(RetentionPolicy.SOURCE) + @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) @IntDef({PURPOSE_CONTROLS, PURPOSE_CLOSE_AD, PURPOSE_OTHER, PURPOSE_NOT_VISIBLE}) public @interface Purpose {} /** Purpose for playback controls overlaying the player. */ - public static final int PURPOSE_CONTROLS = 0; + public static final int PURPOSE_CONTROLS = 1; /** Purpose for ad close buttons overlaying the player. */ - public static final int PURPOSE_CLOSE_AD = 1; + public static final int PURPOSE_CLOSE_AD = 2; /** Purpose for other overlays. */ - public static final int PURPOSE_OTHER = 2; + public static final int PURPOSE_OTHER = 3; /** Purpose for overlays that are not visible. */ - public static final int PURPOSE_NOT_VISIBLE = 3; + public static final int PURPOSE_NOT_VISIBLE = 4; + + /** A builder for {@link AdOverlayInfo} instances. */ + public static final class Builder { + + private final View view; + private final @Purpose int purpose; + + @Nullable private String detailedReason; + + /** + * Creates a new builder. + * + * @param view The view that is overlaying the player. + * @param purpose The purpose of the view. + */ + public Builder(View view, @Purpose int purpose) { + this.view = view; + this.purpose = purpose; + } + + /** + * Sets an optional, detailed reason that the view is on top of the player. + * + * @return This builder, for convenience. + */ + public Builder setDetailedReason(@Nullable String detailedReason) { + this.detailedReason = detailedReason; + return this; + } + + /** Returns a new {@link AdOverlayInfo} instance with the current builder values. */ + // Using deprecated constructor while it still exists. + @SuppressWarnings("deprecation") + public AdOverlayInfo build() { + return new AdOverlayInfo(view, purpose, detailedReason); + } + } /** The overlay view. */ public final View view; /** The purpose of the overlay view. */ - @Purpose public final int purpose; + public final @Purpose int purpose; /** An optional, detailed reason that the overlay view is needed. */ @Nullable public final String reasonDetail; - /** - * Creates a new overlay info. - * - * @param view The view that is overlaying the player. - * @param purpose The purpose of the view. - */ + /** @deprecated Use {@link Builder} instead. */ + @Deprecated public AdOverlayInfo(View view, @Purpose int purpose) { this(view, purpose, /* detailedReason= */ null); } - /** - * Creates a new overlay info. - * - * @param view The view that is overlaying the player. - * @param purpose The purpose of the view. - * @param detailedReason An optional, detailed reason that the view is on top of the player. - */ + /** @deprecated Use {@link Builder} instead. */ + @Deprecated public AdOverlayInfo(View view, @Purpose int purpose, @Nullable String detailedReason) { this.view = view; this.purpose = purpose; From 46ab94bd4152b0addc2253585435186fc9e75582 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 20 Dec 2021 12:22:32 +0000 Subject: [PATCH 33/56] Rename PlayerView to LegacyPlayerView in media3 The old name is kept in exoplayer2. PiperOrigin-RevId: 417378759 --- docs/ui-components.md | 11 ++++++----- .../com/google/android/exoplayer2/ui/PlayerView.java | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/ui-components.md b/docs/ui-components.md index c56107eae7..a9d80c0403 100644 --- a/docs/ui-components.md +++ b/docs/ui-components.md @@ -190,14 +190,15 @@ Note that overriding these drawables will also affect the appearance of All of the view components inflate their layouts from corresponding layout files, which are specified in their Javadoc. For example when a `PlayerControlView` is instantiated, it inflates its layout from -`exo_player_control_view.xml`. To customize these layouts, an application can -define layout files with the same names in its own `res/layout*` directories. -These layout files will override the ones provided by the ExoPlayer library. +`exo_player_control_view.xml`. To customize these layouts, an application +can define layout files with the same names in its own `res/layout*` +directories. These layout files will override the ones provided by the ExoPlayer +library. As an example, suppose we want our playback controls to consist of only a play/pause button positioned in the center of the view. We can achieve this by -creating an `exo_player_control_view.xml` file in the application’s `res/layout` -directory, containing: +creating an `exo_player_control_view.xml` file in the application’s +`res/layout` directory, containing: ~~~ Date: Mon, 20 Dec 2021 18:00:56 +0000 Subject: [PATCH 34/56] Migrate usages of deprecated `MediaSourceFactory` methods PiperOrigin-RevId: 417428182 --- .../com/google/android/exoplayer2/gldemo/MainActivity.java | 4 ++-- .../google/android/exoplayer2/surfacedemo/MainActivity.java | 4 ++-- .../com/google/android/exoplayer2/offline/DownloadHelper.java | 3 ++- .../exoplayer2/source/DefaultMediaSourceFactoryTest.java | 3 +-- .../exoplayer2/source/dash/DefaultMediaSourceFactoryTest.java | 3 +-- .../exoplayer2/source/hls/DefaultMediaSourceFactoryTest.java | 3 +-- .../source/smoothstreaming/DefaultMediaSourceFactoryTest.java | 3 +-- .../android/exoplayer2/playbacktests/gts/DashTestRunner.java | 2 +- 8 files changed, 11 insertions(+), 14 deletions(-) diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java index 48c4ee62dc..1f10cd2c59 100644 --- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java @@ -161,12 +161,12 @@ public final class MainActivity extends Activity { if (type == C.TYPE_DASH) { mediaSource = new DashMediaSource.Factory(dataSourceFactory) - .setDrmSessionManager(drmSessionManager) + .setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager) .createMediaSource(MediaItem.fromUri(uri)); } else if (type == C.TYPE_OTHER) { mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory) - .setDrmSessionManager(drmSessionManager) + .setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager) .createMediaSource(MediaItem.fromUri(uri)); } else { throw new IllegalStateException(); diff --git a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java index c9abfcef14..4433cf50e5 100644 --- a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java +++ b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java @@ -206,12 +206,12 @@ public final class MainActivity extends Activity { if (type == C.TYPE_DASH) { mediaSource = new DashMediaSource.Factory(dataSourceFactory) - .setDrmSessionManager(drmSessionManager) + .setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager) .createMediaSource(MediaItem.fromUri(uri)); } else if (type == C.TYPE_OTHER) { mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory) - .setDrmSessionManager(drmSessionManager) + .setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager) .createMediaSource(MediaItem.fromUri(uri)); } else { throw new IllegalStateException(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 83d609b106..5fef7eb30c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -892,7 +892,8 @@ public final class DownloadHelper { @Nullable DrmSessionManager drmSessionManager) { return new DefaultMediaSourceFactory( dataSourceFactory, ExtractorsFactory.EMPTY, /* serverSideDaiMediaSourceFactory= */ null) - .setDrmSessionManager(drmSessionManager) + .setDrmSessionManagerProvider( + drmSessionManager != null ? unusedMediaItem -> drmSessionManager : null) .createMediaSource(mediaItem); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java index e580d757be..a177274cb4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java @@ -81,8 +81,7 @@ public final class DefaultMediaSourceFactoryTest { MediaSource mediaSource = defaultMediaSourceFactory - .setDrmSessionManager(null) - .setDrmHttpDataSourceFactory(null) + .setDrmSessionManagerProvider(null) .setLoadErrorHandlingPolicy(null) .createMediaSource(mediaItem); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultMediaSourceFactoryTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultMediaSourceFactoryTest.java index 8a607d9110..622ed4dabf 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultMediaSourceFactoryTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultMediaSourceFactoryTest.java @@ -82,8 +82,7 @@ public class DefaultMediaSourceFactoryTest { MediaSource mediaSource = defaultMediaSourceFactory - .setDrmSessionManager(null) - .setDrmHttpDataSourceFactory(null) + .setDrmSessionManagerProvider(null) .setLoadErrorHandlingPolicy(null) .createMediaSource(mediaItem); diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/DefaultMediaSourceFactoryTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/DefaultMediaSourceFactoryTest.java index d67c1fb3ea..d42d557340 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/DefaultMediaSourceFactoryTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/DefaultMediaSourceFactoryTest.java @@ -82,8 +82,7 @@ public class DefaultMediaSourceFactoryTest { MediaSource mediaSource = defaultMediaSourceFactory - .setDrmSessionManager(null) - .setDrmHttpDataSourceFactory(null) + .setDrmSessionManagerProvider(null) .setLoadErrorHandlingPolicy(null) .createMediaSource(mediaItem); diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultMediaSourceFactoryTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultMediaSourceFactoryTest.java index 01face26c6..f81103a54f 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultMediaSourceFactoryTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultMediaSourceFactoryTest.java @@ -93,8 +93,7 @@ public class DefaultMediaSourceFactoryTest { MediaSource mediaSource = defaultMediaSourceFactory - .setDrmSessionManager(null) - .setDrmHttpDataSourceFactory(null) + .setDrmSessionManagerProvider(null) .setLoadErrorHandlingPolicy(null) .createMediaSource(mediaItem); diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index 181412c51c..b3553d3c1c 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -332,7 +332,7 @@ import java.util.List; ? this.dataSourceFactory : new DefaultDataSource.Factory(host); return new DashMediaSource.Factory(dataSourceFactory) - .setDrmSessionManager(drmSessionManager) + .setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager) .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MIN_LOADABLE_RETRY_COUNT)) .createMediaSource(MediaItem.fromUri(manifestUrl)); } From 65c444538bce0b8559582b2a05386633d4d9f1a5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 Dec 2021 08:50:44 +0000 Subject: [PATCH 35/56] Rename HLS master playlist to multivariant playlist The spec renamed this type of playlist in the latest revision to use a more inclusive technical term (see https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-10) PiperOrigin-RevId: 417560274 --- demos/main/src/main/assets/media.exolist.json | 4 +- docs/glossary.md | 2 +- docs/hls.md | 15 +- .../com/google/android/exoplayer2/Format.java | 3 +- .../hls/BundledHlsMediaChunkExtractor.java | 13 +- .../hls/DefaultHlsExtractorFactory.java | 5 +- .../exoplayer2/source/hls/HlsChunkSource.java | 6 +- .../source/hls/HlsExtractorFactory.java | 2 +- .../exoplayer2/source/hls/HlsManifest.java | 33 +- .../exoplayer2/source/hls/HlsMediaChunk.java | 2 +- .../exoplayer2/source/hls/HlsMediaPeriod.java | 99 +++--- .../exoplayer2/source/hls/HlsMediaSource.java | 15 +- .../source/hls/HlsSampleStreamWrapper.java | 37 +- .../source/hls/offline/HlsDownloader.java | 12 +- .../DefaultHlsPlaylistParserFactory.java | 5 +- .../playlist/DefaultHlsPlaylistTracker.java | 55 +-- .../FilteringHlsPlaylistParserFactory.java | 5 +- .../hls/playlist/HlsMasterPlaylist.java | 284 +--------------- .../hls/playlist/HlsMultivariantPlaylist.java | 320 ++++++++++++++++++ .../hls/playlist/HlsPlaylistParser.java | 36 +- .../playlist/HlsPlaylistParserFactory.java | 10 +- .../hls/playlist/HlsPlaylistTracker.java | 22 +- .../source/hls/HlsMediaPeriodTest.java | 21 +- .../hls/offline/HlsDownloadTestData.java | 12 +- .../source/hls/offline/HlsDownloaderTest.java | 30 +- .../DefaultHlsPlaylistTrackerTest.java | 73 ++-- .../playlist/HlsMediaPlaylistParserTest.java | 18 +- ...=> HlsMultivariantPlaylistParserTest.java} | 108 +++--- ...y_master => live_low_latency_multivariant} | 0 ...latency_multivariant_media_uri_with_param} | 0 30 files changed, 683 insertions(+), 564 deletions(-) create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMultivariantPlaylist.java rename library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/{HlsMasterPlaylistParserTest.java => HlsMultivariantPlaylistParserTest.java} (82%) rename testdata/src/test/assets/media/m3u8/{live_low_latency_master => live_low_latency_multivariant} (100%) rename testdata/src/test/assets/media/m3u8/{live_low_latency_master_media_uri_with_param => live_low_latency_multivariant_media_uri_with_param} (100%) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 7659abe5f1..d1db406667 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -237,11 +237,11 @@ "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" }, { - "name": "Apple master playlist advanced (TS)", + "name": "Apple multivariant playlist advanced (TS)", "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8" }, { - "name": "Apple master playlist advanced (FMP4)", + "name": "Apple multivariant playlist advanced (FMP4)", "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8" }, { diff --git a/docs/glossary.md b/docs/glossary.md index 0e8b7d404b..3af6723dfb 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -90,7 +90,7 @@ For more information, see the A file that defines the structure and location of media in [adaptive streaming](#adaptive-streaming) protocols. Examples include -[DASH](#dash) [MPD](#mpd) files, [HLS](#hls) master playlist files and +[DASH](#dash) [MPD](#mpd) files, [HLS](#hls) multivariant playlist files and [Smooth Streaming](#smooth-streaming) manifest files. Not to be confused with an AndroidManifest XML file. diff --git a/docs/hls.md b/docs/hls.md index a6821c3fc5..5633ad8976 100644 --- a/docs/hls.md +++ b/docs/hls.md @@ -30,8 +30,8 @@ If your URI doesn't end with `.m3u8`, you can pass `MimeTypes.APPLICATION_M3U8` to `setMimeType` of `MediaItem.Builder` to explicitly indicate the type of the content. -The URI of the media item may point to either a media playlist or a master -playlist. If the URI points to a master playlist that declares multiple +The URI of the media item may point to either a media playlist or a multivariant +playlist. If the URI points to a multivariant playlist that declares multiple `#EXT-X-STREAM-INF` tags then ExoPlayer will automatically adapt between variants, taking into account both available bandwidth and device capabilities. @@ -89,17 +89,18 @@ app's needs. See the [Customization page][] for examples. ### Disabling chunkless preparation ### By default, ExoPlayer will use chunkless preparation. This means that ExoPlayer -will only use the information in the master playlist to prepare the stream, -which works if the `#EXT-X-STREAM-INF` tags contain the `CODECS` attribute. +will only use the information in the multivariant playlist to prepare the +stream, which works if the `#EXT-X-STREAM-INF` tags contain the `CODECS` +attribute. You may need to disable this feature if your media segments contain muxed -closed-caption tracks that are not declared in the master playlist with a +closed-caption tracks that are not declared in the multivariant playlist with a `#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS` tag. Otherwise, these closed-caption tracks won't be detected and played. You can disable chunkless preparation in the `HlsMediaSource.Factory` as shown in the following snippet. Note that this will increase start up time as ExoPlayer needs to download a media segment to discover these additional tracks and it is preferable to declare the -closed-caption tracks in the master playlist instead. +closed-caption tracks in the multivariant playlist instead. ~~~ HlsMediaSource hlsMediaSource = new HlsMediaSource.Factory(dataSourceFactory) @@ -119,7 +120,7 @@ ExoPlayer][] for a full explanation. The main points are: segments. * Use the `#EXT-X-INDEPENDENT-SEGMENTS` tag. * Prefer demuxed streams, as opposed to files that include both video and audio. -* Include all information you can in the Master Playlist. +* Include all information you can in the Multivariant Playlist. The following guidelines apply specifically for live streams: diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Format.java b/library/common/src/main/java/com/google/android/exoplayer2/Format.java index c21e68c886..375ef7a309 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Format.java @@ -631,7 +631,8 @@ public final class Format implements Bundleable { *
        *
      • DASH representations: Always {@link Format#NO_VALUE}. *
      • HLS variants: The {@code AVERAGE-BANDWIDTH} attribute defined on the corresponding {@code - * EXT-X-STREAM-INF} tag in the master playlist, or {@link Format#NO_VALUE} if not present. + * EXT-X-STREAM-INF} tag in the multivariant playlist, or {@link Format#NO_VALUE} if not + * present. *
      • SmoothStreaming track elements: The {@code Bitrate} attribute defined on the * corresponding {@code TrackElement} in the manifest, or {@link Format#NO_VALUE} if not * present. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/BundledHlsMediaChunkExtractor.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/BundledHlsMediaChunkExtractor.java index 3311d263c6..a7c5081f59 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/BundledHlsMediaChunkExtractor.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/BundledHlsMediaChunkExtractor.java @@ -40,20 +40,20 @@ public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtract private static final PositionHolder POSITION_HOLDER = new PositionHolder(); @VisibleForTesting /* package */ final Extractor extractor; - private final Format masterPlaylistFormat; + private final Format multivariantPlaylistFormat; private final TimestampAdjuster timestampAdjuster; /** * Creates a new instance. * * @param extractor The underlying {@link Extractor}. - * @param masterPlaylistFormat The {@link Format} obtained from the master playlist. + * @param multivariantPlaylistFormat The {@link Format} obtained from the multivariant playlist. * @param timestampAdjuster A {@link TimestampAdjuster} to adjust sample timestamps. */ public BundledHlsMediaChunkExtractor( - Extractor extractor, Format masterPlaylistFormat, TimestampAdjuster timestampAdjuster) { + Extractor extractor, Format multivariantPlaylistFormat, TimestampAdjuster timestampAdjuster) { this.extractor = extractor; - this.masterPlaylistFormat = masterPlaylistFormat; + this.multivariantPlaylistFormat = multivariantPlaylistFormat; this.timestampAdjuster = timestampAdjuster; } @@ -85,7 +85,8 @@ public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtract Assertions.checkState(!isReusable()); Extractor newExtractorInstance; if (extractor instanceof WebvttExtractor) { - newExtractorInstance = new WebvttExtractor(masterPlaylistFormat.language, timestampAdjuster); + newExtractorInstance = + new WebvttExtractor(multivariantPlaylistFormat.language, timestampAdjuster); } else if (extractor instanceof AdtsExtractor) { newExtractorInstance = new AdtsExtractor(); } else if (extractor instanceof Ac3Extractor) { @@ -99,7 +100,7 @@ public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtract "Unexpected extractor type for recreation: " + extractor.getClass().getSimpleName()); } return new BundledHlsMediaChunkExtractor( - newExtractorInstance, masterPlaylistFormat, timestampAdjuster); + newExtractorInstance, multivariantPlaylistFormat, timestampAdjuster); } @Override diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index 2f83f0c78b..ec5510225b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -79,8 +79,9 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { * DefaultTsPayloadReaderFactory} instances. Other flags may be added on top of {@code * payloadReaderFactoryFlags} when creating {@link DefaultTsPayloadReaderFactory}. * @param exposeCea608WhenMissingDeclarations Whether created {@link TsExtractor} instances should - * expose a CEA-608 track should the master playlist contain no Closed Captions declarations. - * If the master playlist contains any Closed Captions declarations, this flag is ignored. + * expose a CEA-608 track should the multivariant playlist contain no Closed Captions + * declarations. If the multivariant playlist contains any Closed Captions declarations, this + * flag is ignored. */ public DefaultHlsExtractorFactory( int payloadReaderFactoryFlags, boolean exposeCea608WhenMissingDeclarations) { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index ba509b3005..13fb2ab6c6 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -154,7 +154,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * {@link HlsChunkSource}s are used for a single playback, they should all share the same * provider. * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption - * information is available in the master playlist. + * information is available in the multivariant playlist. */ public HlsChunkSource( HlsExtractorFactory extractorFactory, @@ -877,8 +877,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public InitializationTrackSelection(TrackGroup group, int[] tracks) { super(group, tracks); - // The initially selected index corresponds to the first EXT-X-STREAMINF tag in the master - // playlist. + // The initially selected index corresponds to the first EXT-X-STREAMINF tag in the + // multivariant playlist. selectedIndex = indexOf(group.getFormat(tracks[0])); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java index 544f222c6a..a137ece7a8 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java @@ -38,7 +38,7 @@ public interface HlsExtractorFactory { * @param uri The URI of the media chunk. * @param format A {@link Format} associated with the chunk to extract. * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption - * information is available in the master playlist. + * information is available in the multivariant playlist. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param responseHeaders The HTTP response headers associated with the media segment or * initialization section to extract. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java index ff2dcf0ba9..00a04b0a89 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java @@ -17,21 +17,42 @@ package com.google.android.exoplayer2.source.hls; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist; -/** Holds a master playlist along with a snapshot of one of its media playlists. */ +/** Holds a multivariant playlist along with a snapshot of one of its media playlists. */ public final class HlsManifest { - /** The master playlist of an HLS stream. */ + /** @deprecated Use {@link #multivariantPlaylist} instead. */ + @Deprecated + @SuppressWarnings("deprecation") // Keeping deprecated field with deprecated class. public final HlsMasterPlaylist masterPlaylist; - /** A snapshot of a media playlist referred to by {@link #masterPlaylist}. */ + /** The multivariant playlist of an HLS stream. */ + public final HlsMultivariantPlaylist multivariantPlaylist; + /** A snapshot of a media playlist referred to by {@link #multivariantPlaylist}. */ public final HlsMediaPlaylist mediaPlaylist; /** - * @param masterPlaylist The master playlist. + * @param multivariantPlaylist The multivariant playlist. * @param mediaPlaylist The media playlist. */ - HlsManifest(HlsMasterPlaylist masterPlaylist, HlsMediaPlaylist mediaPlaylist) { - this.masterPlaylist = masterPlaylist; + @SuppressWarnings("deprecation") // Intentionally creating deprecated hlsMasterPlaylist field. + /* package */ HlsManifest( + HlsMultivariantPlaylist multivariantPlaylist, HlsMediaPlaylist mediaPlaylist) { + this.multivariantPlaylist = multivariantPlaylist; this.mediaPlaylist = mediaPlaylist; + this.masterPlaylist = + new HlsMasterPlaylist( + multivariantPlaylist.baseUri, + multivariantPlaylist.tags, + multivariantPlaylist.variants, + multivariantPlaylist.videos, + multivariantPlaylist.audios, + multivariantPlaylist.subtitles, + multivariantPlaylist.closedCaptions, + multivariantPlaylist.muxedAudioFormat, + multivariantPlaylist.muxedCaptionFormats, + multivariantPlaylist.hasIndependentSegments, + multivariantPlaylist.variableDefinitions, + multivariantPlaylist.sessionKeyDrmInitData); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index e405bfc374..d0711a6d27 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -64,7 +64,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * @param segmentBaseHolder The segment holder. * @param playlistUrl The url of the playlist from which this chunk was obtained. * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption - * information is available in the master playlist. + * information is available in the multivariant playlist. * @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionData See {@link #trackSelectionData}. * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 76368135e1..7ad46c69fa 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -36,9 +36,9 @@ import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant; +import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist.Rendition; +import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist.Variant; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.upstream.Allocator; @@ -178,17 +178,18 @@ public final class HlsMediaPeriod return Assertions.checkNotNull(trackGroups); } - // TODO: When the master playlist does not de-duplicate variants by URL and allows Renditions with - // null URLs, this method must be updated to calculate stream keys that are compatible with those - // that may already be persisted for offline. + // TODO: When the multivariant playlist does not de-duplicate variants by URL and allows + // Renditions with null URLs, this method must be updated to calculate stream keys that are + // compatible with those that may already be persisted for offline. @Override public List getStreamKeys(List trackSelections) { - // See HlsMasterPlaylist.copy for interpretation of StreamKeys. - HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist()); - boolean hasVariants = !masterPlaylist.variants.isEmpty(); + // See HlsMultivariantPlaylist.copy for interpretation of StreamKeys. + HlsMultivariantPlaylist multivariantPlaylist = + Assertions.checkNotNull(playlistTracker.getMultivariantPlaylist()); + boolean hasVariants = !multivariantPlaylist.variants.isEmpty(); int audioWrapperOffset = hasVariants ? 1 : 0; // Subtitle sample stream wrappers are held last. - int subtitleWrapperOffset = sampleStreamWrappers.length - masterPlaylist.subtitles.size(); + int subtitleWrapperOffset = sampleStreamWrappers.length - multivariantPlaylist.subtitles.size(); TrackGroupArray mainWrapperTrackGroups; int mainWrapperPrimaryGroupIndex; @@ -216,7 +217,8 @@ public final class HlsMediaPeriod hasPrimaryTrackGroupSelection = true; for (int i = 0; i < trackSelection.length(); i++) { int variantIndex = mainWrapperVariantIndices[trackSelection.getIndexInTrackGroup(i)]; - streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, variantIndex)); + streamKeys.add( + new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, variantIndex)); } } else { // Embedded group in main wrapper. @@ -230,8 +232,8 @@ public final class HlsMediaPeriod if (selectedTrackGroupIndex != C.INDEX_UNSET) { int groupIndexType = i < subtitleWrapperOffset - ? HlsMasterPlaylist.GROUP_INDEX_AUDIO - : HlsMasterPlaylist.GROUP_INDEX_SUBTITLE; + ? HlsMultivariantPlaylist.GROUP_INDEX_AUDIO + : HlsMultivariantPlaylist.GROUP_INDEX_SUBTITLE; int[] selectedWrapperUrlIndices = manifestUrlIndicesPerWrapper[i]; for (int trackIndex = 0; trackIndex < trackSelection.length(); trackIndex++) { int renditionIndex = @@ -247,16 +249,18 @@ public final class HlsMediaPeriod // A track selection includes a variant-embedded track, but no variant is added yet. We use // the valid variant with the lowest bitrate to reduce overhead. int lowestBitrateIndex = mainWrapperVariantIndices[0]; - int lowestBitrate = masterPlaylist.variants.get(mainWrapperVariantIndices[0]).format.bitrate; + int lowestBitrate = + multivariantPlaylist.variants.get(mainWrapperVariantIndices[0]).format.bitrate; for (int i = 1; i < mainWrapperVariantIndices.length; i++) { int variantBitrate = - masterPlaylist.variants.get(mainWrapperVariantIndices[i]).format.bitrate; + multivariantPlaylist.variants.get(mainWrapperVariantIndices[i]).format.bitrate; if (variantBitrate < lowestBitrate) { lowestBitrate = variantBitrate; lowestBitrateIndex = mainWrapperVariantIndices[i]; } } - streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, lowestBitrateIndex)); + streamKeys.add( + new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, lowestBitrateIndex)); } return streamKeys; } @@ -489,15 +493,16 @@ public final class HlsMediaPeriod // Internal methods. private void buildAndPrepareSampleStreamWrappers(long positionUs) { - HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist()); + HlsMultivariantPlaylist multivariantPlaylist = + Assertions.checkNotNull(playlistTracker.getMultivariantPlaylist()); Map overridingDrmInitData = useSessionKeys - ? deriveOverridingDrmInitData(masterPlaylist.sessionKeyDrmInitData) + ? deriveOverridingDrmInitData(multivariantPlaylist.sessionKeyDrmInitData) : Collections.emptyMap(); - boolean hasVariants = !masterPlaylist.variants.isEmpty(); - List audioRenditions = masterPlaylist.audios; - List subtitleRenditions = masterPlaylist.subtitles; + boolean hasVariants = !multivariantPlaylist.variants.isEmpty(); + List audioRenditions = multivariantPlaylist.audios; + List subtitleRenditions = multivariantPlaylist.subtitles; pendingPrepareCount = 0; ArrayList sampleStreamWrappers = new ArrayList<>(); @@ -505,7 +510,7 @@ public final class HlsMediaPeriod if (hasVariants) { buildAndPrepareMainSampleStreamWrapper( - masterPlaylist, + multivariantPlaylist, positionUs, sampleStreamWrappers, manifestUrlIndicesPerWrapper, @@ -523,7 +528,8 @@ public final class HlsMediaPeriod audioVideoSampleStreamWrapperCount = sampleStreamWrappers.size(); - // Subtitle stream wrappers. We can always use master playlist information to prepare these. + // Subtitle stream wrappers. We can always use multivariant playlist information to prepare + // these. for (int i = 0; i < subtitleRenditions.size(); i++) { Rendition subtitleRendition = subtitleRenditions.get(i); String sampleStreamWrapperUid = "subtitle:" + i + ":" + subtitleRendition.name; @@ -539,7 +545,7 @@ public final class HlsMediaPeriod positionUs); manifestUrlIndicesPerWrapper.add(new int[] {i}); sampleStreamWrappers.add(sampleStreamWrapper); - sampleStreamWrapper.prepareWithMasterPlaylistInfo( + sampleStreamWrapper.prepareWithMultivariantPlaylistInfo( new TrackGroup[] {new TrackGroup(sampleStreamWrapperUid, subtitleRendition.format)}, /* primaryTrackGroupIndex= */ 0); } @@ -560,8 +566,8 @@ public final class HlsMediaPeriod * This method creates and starts preparation of the main {@link HlsSampleStreamWrapper}. * *

        The main sample stream wrapper is the first element of {@link #sampleStreamWrappers}. It - * provides {@link SampleStream}s for the variant urls in the master playlist. It may be adaptive - * and may contain multiple muxed tracks. + * provides {@link SampleStream}s for the variant urls in the multivariant playlist. It may be + * adaptive and may contain multiple muxed tracks. * *

        If chunkless preparation is allowed, the media period will try preparation without segment * downloads. This is only possible if variants contain the CODECS attribute. If not, traditional @@ -570,13 +576,13 @@ public final class HlsMediaPeriod * *

          *
        • A muxed audio track will be exposed if the codecs list contain an audio entry and the - * master playlist either contains an EXT-X-MEDIA tag without the URI attribute or does not - * contain any EXT-X-MEDIA tag. - *
        • Closed captions will only be exposed if they are declared by the master playlist. + * multivariant playlist either contains an EXT-X-MEDIA tag without the URI attribute or + * does not contain any EXT-X-MEDIA tag. + *
        • Closed captions will only be exposed if they are declared by the multivariant playlist. *
        • An ID3 track is exposed preemptively, in case the segments contain an ID3 track. *
        * - * @param masterPlaylist The HLS master playlist. + * @param multivariantPlaylist The HLS multivariant playlist. * @param positionUs If preparation requires any chunk downloads, the position in microseconds at * which downloading should start. Ignored otherwise. * @param sampleStreamWrappers List to which the built main sample stream wrapper should be added. @@ -585,16 +591,16 @@ public final class HlsMediaPeriod * (i.e. {@link DrmInitData#schemeType}). */ private void buildAndPrepareMainSampleStreamWrapper( - HlsMasterPlaylist masterPlaylist, + HlsMultivariantPlaylist multivariantPlaylist, long positionUs, List sampleStreamWrappers, List manifestUrlIndicesPerWrapper, Map overridingDrmInitData) { - int[] variantTypes = new int[masterPlaylist.variants.size()]; + int[] variantTypes = new int[multivariantPlaylist.variants.size()]; int videoVariantCount = 0; int audioVariantCount = 0; - for (int i = 0; i < masterPlaylist.variants.size(); i++) { - Variant variant = masterPlaylist.variants.get(i); + for (int i = 0; i < multivariantPlaylist.variants.size(); i++) { + Variant variant = multivariantPlaylist.variants.get(i); Format format = variant.format; if (format.height > 0 || Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_VIDEO) != null) { variantTypes[i] = C.TRACK_TYPE_VIDEO; @@ -611,8 +617,8 @@ public final class HlsMediaPeriod int selectedVariantsCount = variantTypes.length; if (videoVariantCount > 0) { // We've identified some variants as definitely containing video. Assume variants within the - // master playlist are marked consistently, and hence that we have the full set. Filter out - // any other variants, which are likely to be audio only. + // multivariant playlist are marked consistently, and hence that we have the full set. Filter + // out any other variants, which are likely to be audio only. useVideoVariantsOnly = true; selectedVariantsCount = videoVariantCount; } else if (audioVariantCount < variantTypes.length) { @@ -625,10 +631,10 @@ public final class HlsMediaPeriod Format[] selectedPlaylistFormats = new Format[selectedVariantsCount]; int[] selectedVariantIndices = new int[selectedVariantsCount]; int outIndex = 0; - for (int i = 0; i < masterPlaylist.variants.size(); i++) { + for (int i = 0; i < multivariantPlaylist.variants.size(); i++) { if ((!useVideoVariantsOnly || variantTypes[i] == C.TRACK_TYPE_VIDEO) && (!useNonAudioVariantsOnly || variantTypes[i] != C.TRACK_TYPE_AUDIO)) { - Variant variant = masterPlaylist.variants.get(i); + Variant variant = multivariantPlaylist.variants.get(i); selectedPlaylistUrls[outIndex] = variant.url; selectedPlaylistFormats[outIndex] = variant.format; selectedVariantIndices[outIndex++] = i; @@ -653,8 +659,8 @@ public final class HlsMediaPeriod trackType, selectedPlaylistUrls, selectedPlaylistFormats, - masterPlaylist.muxedAudioFormat, - masterPlaylist.muxedCaptionFormats, + multivariantPlaylist.muxedAudioFormat, + multivariantPlaylist.muxedCaptionFormats, overridingDrmInitData, positionUs); sampleStreamWrappers.add(sampleStreamWrapper); @@ -669,16 +675,17 @@ public final class HlsMediaPeriod muxedTrackGroups.add(new TrackGroup(sampleStreamWrapperUid, videoFormats)); if (numberOfAudioCodecs > 0 - && (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) { + && (multivariantPlaylist.muxedAudioFormat != null + || multivariantPlaylist.audios.isEmpty())) { muxedTrackGroups.add( new TrackGroup( /* id= */ sampleStreamWrapperUid + ":audio", deriveAudioFormat( selectedPlaylistFormats[0], - masterPlaylist.muxedAudioFormat, + multivariantPlaylist.muxedAudioFormat, /* isPrimaryTrackInVariant= */ false))); } - List ccFormats = masterPlaylist.muxedCaptionFormats; + List ccFormats = multivariantPlaylist.muxedCaptionFormats; if (ccFormats != null) { for (int i = 0; i < ccFormats.size(); i++) { String ccId = sampleStreamWrapperUid + ":cc:" + i; @@ -692,7 +699,7 @@ public final class HlsMediaPeriod audioFormats[i] = deriveAudioFormat( /* variantFormat= */ selectedPlaylistFormats[i], - masterPlaylist.muxedAudioFormat, + multivariantPlaylist.muxedAudioFormat, /* isPrimaryTrackInVariant= */ true); } muxedTrackGroups.add(new TrackGroup(sampleStreamWrapperUid, audioFormats)); @@ -707,7 +714,7 @@ public final class HlsMediaPeriod .build()); muxedTrackGroups.add(id3TrackGroup); - sampleStreamWrapper.prepareWithMasterPlaylistInfo( + sampleStreamWrapper.prepareWithMultivariantPlaylistInfo( muxedTrackGroups.toArray(new TrackGroup[0]), /* primaryTrackGroupIndex= */ 0, /* optionalTrackGroupsIndices...= */ muxedTrackGroups.indexOf(id3TrackGroup)); @@ -768,7 +775,7 @@ public final class HlsMediaPeriod if (allowChunklessPreparation && codecStringsAllowChunklessPreparation) { Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]); - sampleStreamWrapper.prepareWithMasterPlaylistInfo( + sampleStreamWrapper.prepareWithMultivariantPlaylistInfo( new TrackGroup[] {new TrackGroup(sampleStreamWrapperUid, renditionFormats)}, /* primaryTrackGroupIndex= */ 0); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index e0aaafc3d5..152f6d9abe 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -238,7 +238,8 @@ public final class HlsMediaSource extends BaseMediaSource /** * Sets whether chunkless preparation is allowed. If true, preparation without chunk downloads - * will be enabled for streams that provide sufficient information in their master playlist. + * will be enabled for streams that provide sufficient information in their multivariant + * playlist. * * @param allowChunklessPreparation Whether chunkless preparation is allowed. * @return This factory, for convenience. @@ -273,10 +274,10 @@ public final class HlsMediaSource extends BaseMediaSource } /** - * Sets whether to use #EXT-X-SESSION-KEY tags provided in the master playlist. If enabled, it's - * assumed that any single session key declared in the master playlist can be used to obtain all - * of the keys required for playback. For media where this is not true, this option should not - * be enabled. + * Sets whether to use #EXT-X-SESSION-KEY tags provided in the multivariant playlist. If + * enabled, it's assumed that any single session key declared in the multivariant playlist can + * be used to obtain all of the keys required for playback. For media where this is not true, + * this option should not be enabled. * * @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags. * @return This factory, for convenience. @@ -524,9 +525,9 @@ public final class HlsMediaSource extends BaseMediaSource || mediaPlaylist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD ? windowStartTimeMs : C.TIME_UNSET; - // The master playlist is non-null because the first playlist has been fetched by now. + // The multivariant playlist is non-null because the first playlist has been fetched by now. HlsManifest manifest = - new HlsManifest(checkNotNull(playlistTracker.getMasterPlaylist()), mediaPlaylist); + new HlsManifest(checkNotNull(playlistTracker.getMultivariantPlaylist()), mediaPlaylist); SinglePeriodTimeline timeline = playlistTracker.isLive() ? createTimelineForLive( diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 4a172320c0..09221b2b0a 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -103,7 +103,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * Called when the wrapper has been prepared. * *

        Note: This method will be called on a later handler loop than the one on which either - * {@link #prepareWithMasterPlaylistInfo} or {@link #continuePreparing} are invoked. + * {@link #prepareWithMultivariantPlaylistInfo} or {@link #continuePreparing} are invoked. */ void onPrepared(); @@ -197,7 +197,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * stream's {@link DrmInitData} will be overridden. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The position from which to start loading media. - * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. + * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the multivariant + * playlist. * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession * DrmSessions} with. * @param drmEventDispatcher A dispatcher to notify of {@link DrmSessionEventListener} events. @@ -261,7 +262,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } /** - * Prepares the sample stream wrapper with master playlist information. + * Prepares the sample stream wrapper with multivariant playlist information. * * @param trackGroups The {@link TrackGroup TrackGroups} to expose through {@link * #getTrackGroups()}. @@ -269,7 +270,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * @param optionalTrackGroupsIndices The indices of any {@code trackGroups} that should not * trigger a failure if not found in the media playlist's segments. */ - public void prepareWithMasterPlaylistInfo( + public void prepareWithMultivariantPlaylistInfo( TrackGroup[] trackGroups, int primaryTrackGroupIndex, int... optionalTrackGroupsIndices) { this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups); optionalTrackGroups = new HashSet<>(); @@ -1298,8 +1299,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } } if (trackGroups != null) { - // The track groups were created with master playlist information. They only need to be mapped - // to a sample queue. + // The track groups were created with multivariant playlist information. They only need to be + // mapped to a sample queue. mapSampleQueuesToMatchTrackGroups(); } else { // Tracks are created using media segment information. @@ -1334,18 +1335,18 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * Builds tracks that are exposed by this {@link HlsSampleStreamWrapper} instance, as well as * internal data-structures required for operation. * - *

        Tracks in HLS are complicated. A HLS master playlist contains a number of "variants". Each - * variant stream typically contains muxed video, audio and (possibly) additional audio, metadata - * and caption tracks. We wish to allow the user to select between an adaptive track that spans - * all variants, as well as each individual variant. If multiple audio tracks are present within - * each variant then we wish to allow the user to select between those also. + *

        Tracks in HLS are complicated. A HLS multivariant playlist contains a number of "variants". + * Each variant stream typically contains muxed video, audio and (possibly) additional audio, + * metadata and caption tracks. We wish to allow the user to select between an adaptive track that + * spans all variants, as well as each individual variant. If multiple audio tracks are present + * within each variant then we wish to allow the user to select between those also. * *

        To do this, tracks are constructed as follows. The {@link HlsChunkSource} exposes (N+1) - * tracks, where N is the number of variants defined in the HLS master playlist. These consist of - * one adaptive track defined to span all variants and a track for each individual variant. The - * adaptive track is initially selected. The extractor is then prepared to discover the tracks - * inside of each variant stream. The two sets of tracks are then combined by this method to - * create a third set, which is the set exposed by this {@link HlsSampleStreamWrapper}: + * tracks, where N is the number of variants defined in the HLS multivariant playlist. These + * consist of one adaptive track defined to span all variants and a track for each individual + * variant. The adaptive track is initially selected. The extractor is then prepared to discover + * the tracks inside of each variant stream. The two sets of tracks are then combined by this + * method to create a third set, which is the set exposed by this {@link HlsSampleStreamWrapper}: * *

          *
        • The extractor tracks are inspected to infer a "primary" track type. If a video track is @@ -1518,14 +1519,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } /** - * Derives a track sample format from the corresponding format in the master playlist, and a + * Derives a track sample format from the corresponding format in the multivariant playlist, and a * sample format that may have been obtained from a chunk belonging to a different track in the * same track group. * *

          Note: Since the sample format may have been obtained from a chunk belonging to a different * track, it should not be used as a source for data that may vary between tracks. * - * @param playlistFormat The format information obtained from the master playlist. + * @param playlistFormat The format information obtained from the multivariant playlist. * @param sampleFormat The format information obtained from samples within a chunk. The chunk may * belong to a different track in the same track group. * @param propagateBitrates Whether the bitrates from the playlist format should be included in diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java index 0fe719c263..99488539f2 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java @@ -19,8 +19,8 @@ import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.offline.SegmentDownloader; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer2.upstream.DataSource; @@ -45,14 +45,14 @@ import java.util.concurrent.Executor; * new CacheDataSource.Factory() * .setCache(cache) * .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory()); - * // Create a downloader for the first variant in a master playlist. + * // Create a downloader for the first variant in a multivariant playlist. * HlsDownloader hlsDownloader = * new HlsDownloader( * new MediaItem.Builder() * .setUri(playlistUri) * .setStreamKeys( * Collections.singletonList( - * new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0))) + * new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, 0))) * .build(), * Collections.singletonList(); * // Perform the download. @@ -113,9 +113,9 @@ public final class HlsDownloader extends SegmentDownloader { protected List getSegments(DataSource dataSource, HlsPlaylist playlist, boolean removing) throws IOException, InterruptedException { ArrayList mediaPlaylistDataSpecs = new ArrayList<>(); - if (playlist instanceof HlsMasterPlaylist) { - HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; - addMediaPlaylistDataSpecs(masterPlaylist.mediaPlaylistUrls, mediaPlaylistDataSpecs); + if (playlist instanceof HlsMultivariantPlaylist) { + HlsMultivariantPlaylist multivariantPlaylist = (HlsMultivariantPlaylist) playlist; + addMediaPlaylistDataSpecs(multivariantPlaylist.mediaPlaylistUrls, mediaPlaylistDataSpecs); } else { mediaPlaylistDataSpecs.add( SegmentDownloader.getCompressibleDataSpec(Uri.parse(playlist.baseUri))); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java index 3bb34a0268..ba3312dfd6 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java @@ -28,7 +28,8 @@ public final class DefaultHlsPlaylistParserFactory implements HlsPlaylistParserF @Override public ParsingLoadable.Parser createPlaylistParser( - HlsMasterPlaylist masterPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist) { - return new HlsPlaylistParser(masterPlaylist, previousMediaPlaylist); + HlsMultivariantPlaylist multivariantPlaylist, + @Nullable HlsMediaPlaylist previousMediaPlaylist) { + return new HlsPlaylistParser(multivariantPlaylist, previousMediaPlaylist); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java index 68d300966e..c9114855e9 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -29,10 +29,10 @@ import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Part; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.RenditionReport; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; +import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist.Variant; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; @@ -72,7 +72,7 @@ public final class DefaultHlsPlaylistTracker @Nullable private Loader initialPlaylistLoader; @Nullable private Handler playlistRefreshHandler; @Nullable private PrimaryPlaylistListener primaryPlaylistListener; - @Nullable private HlsMasterPlaylist masterPlaylist; + @Nullable private HlsMultivariantPlaylist multivariantPlaylist; @Nullable private Uri primaryMediaPlaylistUrl; @Nullable private HlsMediaPlaylist primaryMediaPlaylistSnapshot; private boolean isLive; @@ -131,30 +131,33 @@ public final class DefaultHlsPlaylistTracker this.playlistRefreshHandler = Util.createHandlerForCurrentLooper(); this.eventDispatcher = eventDispatcher; this.primaryPlaylistListener = primaryPlaylistListener; - ParsingLoadable masterPlaylistLoadable = + ParsingLoadable multivariantPlaylistLoadable = new ParsingLoadable<>( dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), initialPlaylistUri, C.DATA_TYPE_MANIFEST, playlistParserFactory.createPlaylistParser()); Assertions.checkState(initialPlaylistLoader == null); - initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MasterPlaylist"); + initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MultivariantPlaylist"); long elapsedRealtime = initialPlaylistLoader.startLoading( - masterPlaylistLoadable, + multivariantPlaylistLoadable, this, - loadErrorHandlingPolicy.getMinimumLoadableRetryCount(masterPlaylistLoadable.type)); + loadErrorHandlingPolicy.getMinimumLoadableRetryCount( + multivariantPlaylistLoadable.type)); eventDispatcher.loadStarted( new LoadEventInfo( - masterPlaylistLoadable.loadTaskId, masterPlaylistLoadable.dataSpec, elapsedRealtime), - masterPlaylistLoadable.type); + multivariantPlaylistLoadable.loadTaskId, + multivariantPlaylistLoadable.dataSpec, + elapsedRealtime), + multivariantPlaylistLoadable.type); } @Override public void stop() { primaryMediaPlaylistUrl = null; primaryMediaPlaylistSnapshot = null; - masterPlaylist = null; + multivariantPlaylist = null; initialStartTimeUs = C.TIME_UNSET; initialPlaylistLoader.release(); initialPlaylistLoader = null; @@ -179,8 +182,8 @@ public final class DefaultHlsPlaylistTracker @Override @Nullable - public HlsMasterPlaylist getMasterPlaylist() { - return masterPlaylist; + public HlsMultivariantPlaylist getMultivariantPlaylist() { + return multivariantPlaylist; } @Override @@ -243,18 +246,19 @@ public final class DefaultHlsPlaylistTracker public void onLoadCompleted( ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { HlsPlaylist result = loadable.getResult(); - HlsMasterPlaylist masterPlaylist; + HlsMultivariantPlaylist multivariantPlaylist; boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; if (isMediaPlaylist) { - masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); - } else /* result instanceof HlsMasterPlaylist */ { - masterPlaylist = (HlsMasterPlaylist) result; + multivariantPlaylist = + HlsMultivariantPlaylist.createSingleVariantMultivariantPlaylist(result.baseUri); + } else /* result instanceof HlsMultivariantPlaylist */ { + multivariantPlaylist = (HlsMultivariantPlaylist) result; } - this.masterPlaylist = masterPlaylist; - primaryMediaPlaylistUrl = masterPlaylist.variants.get(0).url; + this.multivariantPlaylist = multivariantPlaylist; + primaryMediaPlaylistUrl = multivariantPlaylist.variants.get(0).url; // Add a temporary playlist listener for loading the first primary playlist. listeners.add(new FirstPrimaryMediaPlaylistListener()); - createBundles(masterPlaylist.mediaPlaylistUrls); + createBundles(multivariantPlaylist.mediaPlaylistUrls); LoadEventInfo loadEventInfo = new LoadEventInfo( loadable.loadTaskId, @@ -327,7 +331,7 @@ public final class DefaultHlsPlaylistTracker // Internal methods. private boolean maybeSelectNewPrimaryUrl() { - List variants = masterPlaylist.variants; + List variants = multivariantPlaylist.variants; int variantsSize = variants.size(); long currentTimeMs = SystemClock.elapsedRealtime(); for (int i = 0; i < variantsSize; i++) { @@ -382,9 +386,12 @@ public final class DefaultHlsPlaylistTracker return newPrimaryPlaylistUri; } - /** Returns whether any of the variants in the master playlist have the specified playlist URL. */ + /** + * Returns whether any of the variants in the multivariant playlist have the specified playlist + * URL. + */ private boolean isVariantUrl(Uri playlistUrl) { - List variants = masterPlaylist.variants; + List variants = multivariantPlaylist.variants; for (int i = 0; i < variants.size(); i++) { if (playlistUrl.equals(variants.get(i).url)) { return true; @@ -687,7 +694,7 @@ public final class DefaultHlsPlaylistTracker private void loadPlaylistImmediately(Uri playlistRequestUri) { ParsingLoadable.Parser mediaPlaylistParser = - playlistParserFactory.createPlaylistParser(masterPlaylist, playlistSnapshot); + playlistParserFactory.createPlaylistParser(multivariantPlaylist, playlistSnapshot); ParsingLoadable mediaPlaylistLoadable = new ParsingLoadable<>( mediaPlaylistDataSource, @@ -823,7 +830,7 @@ public final class DefaultHlsPlaylistTracker if (primaryMediaPlaylistSnapshot == null) { long nowMs = SystemClock.elapsedRealtime(); int variantExclusionCounter = 0; - List variants = castNonNull(masterPlaylist).variants; + List variants = castNonNull(multivariantPlaylist).variants; for (int i = 0; i < variants.size(); i++) { @Nullable MediaPlaylistBundle mediaPlaylistBundle = playlistBundles.get(variants.get(i).url); @@ -835,7 +842,7 @@ public final class DefaultHlsPlaylistTracker new LoadErrorHandlingPolicy.FallbackOptions( /* numberOfLocations= */ 1, /* numberOfExcludedLocations= */ 0, - /* numberOfTracks= */ masterPlaylist.variants.size(), + /* numberOfTracks= */ multivariantPlaylist.variants.size(), /* numberOfExcludedTracks= */ variantExclusionCounter); @Nullable LoadErrorHandlingPolicy.FallbackSelection fallbackSelection = diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java index cab93a1dc3..e556ee099e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java @@ -49,9 +49,10 @@ public final class FilteringHlsPlaylistParserFactory implements HlsPlaylistParse @Override public ParsingLoadable.Parser createPlaylistParser( - HlsMasterPlaylist masterPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist) { + HlsMultivariantPlaylist multivariantPlaylist, + @Nullable HlsMediaPlaylist previousMediaPlaylist) { return new FilteringManifestParser<>( - hlsPlaylistParserFactory.createPlaylistParser(masterPlaylist, previousMediaPlaylist), + hlsPlaylistParserFactory.createPlaylistParser(multivariantPlaylist, previousMediaPlaylist), streamKeys); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index 57f4100515..626edfa6ce 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -15,181 +15,22 @@ */ package com.google.android.exoplayer2.source.hls.playlist; -import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; -/** Represents an HLS master playlist. */ -public final class HlsMasterPlaylist extends HlsPlaylist { - - /** Represents an empty master playlist, from which no attributes can be inherited. */ - public static final HlsMasterPlaylist EMPTY = - new HlsMasterPlaylist( - /* baseUri= */ "", - /* tags= */ Collections.emptyList(), - /* variants= */ Collections.emptyList(), - /* videos= */ Collections.emptyList(), - /* audios= */ Collections.emptyList(), - /* subtitles= */ Collections.emptyList(), - /* closedCaptions= */ Collections.emptyList(), - /* muxedAudioFormat= */ null, - /* muxedCaptionFormats= */ Collections.emptyList(), - /* hasIndependentSegments= */ false, - /* variableDefinitions= */ Collections.emptyMap(), - /* sessionKeyDrmInitData= */ Collections.emptyList()); - - // These constants must not be changed because they are persisted in offline stream keys. - public static final int GROUP_INDEX_VARIANT = 0; - public static final int GROUP_INDEX_AUDIO = 1; - public static final int GROUP_INDEX_SUBTITLE = 2; - - /** A variant (i.e. an #EXT-X-STREAM-INF tag) in a master playlist. */ - public static final class Variant { - - /** The variant's url. */ - public final Uri url; - - /** Format information associated with this variant. */ - public final Format format; - - /** The video rendition group referenced by this variant, or {@code null}. */ - @Nullable public final String videoGroupId; - - /** The audio rendition group referenced by this variant, or {@code null}. */ - @Nullable public final String audioGroupId; - - /** The subtitle rendition group referenced by this variant, or {@code null}. */ - @Nullable public final String subtitleGroupId; - - /** The caption rendition group referenced by this variant, or {@code null}. */ - @Nullable public final String captionGroupId; - - /** - * @param url See {@link #url}. - * @param format See {@link #format}. - * @param videoGroupId See {@link #videoGroupId}. - * @param audioGroupId See {@link #audioGroupId}. - * @param subtitleGroupId See {@link #subtitleGroupId}. - * @param captionGroupId See {@link #captionGroupId}. - */ - public Variant( - Uri url, - Format format, - @Nullable String videoGroupId, - @Nullable String audioGroupId, - @Nullable String subtitleGroupId, - @Nullable String captionGroupId) { - this.url = url; - this.format = format; - this.videoGroupId = videoGroupId; - this.audioGroupId = audioGroupId; - this.subtitleGroupId = subtitleGroupId; - this.captionGroupId = captionGroupId; - } - - /** - * Creates a variant for a given media playlist url. - * - * @param url The media playlist url. - * @return The variant instance. - */ - public static Variant createMediaPlaylistVariantUrl(Uri url) { - Format format = - new Format.Builder().setId("0").setContainerMimeType(MimeTypes.APPLICATION_M3U8).build(); - return new Variant( - url, - format, - /* videoGroupId= */ null, - /* audioGroupId= */ null, - /* subtitleGroupId= */ null, - /* captionGroupId= */ null); - } - - /** Returns a copy of this instance with the given {@link Format}. */ - public Variant copyWithFormat(Format format) { - return new Variant(url, format, videoGroupId, audioGroupId, subtitleGroupId, captionGroupId); - } - } - - /** A rendition (i.e. an #EXT-X-MEDIA tag) in a master playlist. */ - public static final class Rendition { - - /** The rendition's url, or null if the tag does not have a URI attribute. */ - @Nullable public final Uri url; - - /** Format information associated with this rendition. */ - public final Format format; - - /** The group to which this rendition belongs. */ - public final String groupId; - - /** The name of the rendition. */ - public final String name; - - /** - * @param url See {@link #url}. - * @param format See {@link #format}. - * @param groupId See {@link #groupId}. - * @param name See {@link #name}. - */ - public Rendition(@Nullable Uri url, Format format, String groupId, String name) { - this.url = url; - this.format = format; - this.groupId = groupId; - this.name = name; - } - } - - /** All of the media playlist URLs referenced by the playlist. */ - public final List mediaPlaylistUrls; - /** The variants declared by the playlist. */ - public final List variants; - /** The video renditions declared by the playlist. */ - public final List videos; - /** The audio renditions declared by the playlist. */ - public final List audios; - /** The subtitle renditions declared by the playlist. */ - public final List subtitles; - /** The closed caption renditions declared by the playlist. */ - public final List closedCaptions; +/** @deprecated Use {@link HlsMultivariantPlaylist} instead. */ +@Deprecated +public final class HlsMasterPlaylist extends HlsMultivariantPlaylist { /** - * The format of the audio muxed in the variants. May be null if the playlist does not declare any - * muxed audio. - */ - @Nullable public final Format muxedAudioFormat; - /** - * The format of the closed captions declared by the playlist. May be empty if the playlist - * explicitly declares no captions are available, or null if the playlist does not declare any - * captions information. - */ - @Nullable public final List muxedCaptionFormats; - /** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */ - public final Map variableDefinitions; - /** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */ - public final List sessionKeyDrmInitData; - - /** - * @param baseUri See {@link #baseUri}. - * @param tags See {@link #tags}. - * @param variants See {@link #variants}. - * @param videos See {@link #videos}. - * @param audios See {@link #audios}. - * @param subtitles See {@link #subtitles}. - * @param closedCaptions See {@link #closedCaptions}. - * @param muxedAudioFormat See {@link #muxedAudioFormat}. - * @param muxedCaptionFormats See {@link #muxedCaptionFormats}. - * @param hasIndependentSegments See {@link #hasIndependentSegments}. - * @param variableDefinitions See {@link #variableDefinitions}. - * @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}. + * Creates an HLS multivariant playlist. + * + * @deprecated Use {@link HlsMultivariantPlaylist#HlsMultivariantPlaylist} instead. */ + @Deprecated public HlsMasterPlaylist( String baseUri, List tags, @@ -203,117 +44,18 @@ public final class HlsMasterPlaylist extends HlsPlaylist { boolean hasIndependentSegments, Map variableDefinitions, List sessionKeyDrmInitData) { - super(baseUri, tags, hasIndependentSegments); - this.mediaPlaylistUrls = - Collections.unmodifiableList( - getMediaPlaylistUrls(variants, videos, audios, subtitles, closedCaptions)); - this.variants = Collections.unmodifiableList(variants); - this.videos = Collections.unmodifiableList(videos); - this.audios = Collections.unmodifiableList(audios); - this.subtitles = Collections.unmodifiableList(subtitles); - this.closedCaptions = Collections.unmodifiableList(closedCaptions); - this.muxedAudioFormat = muxedAudioFormat; - this.muxedCaptionFormats = - muxedCaptionFormats != null ? Collections.unmodifiableList(muxedCaptionFormats) : null; - this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions); - this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData); - } - - @Override - public HlsMasterPlaylist copy(List streamKeys) { - return new HlsMasterPlaylist( + super( baseUri, tags, - copyStreams(variants, GROUP_INDEX_VARIANT, streamKeys), - // TODO: Allow stream keys to specify video renditions to be retained. - /* videos= */ Collections.emptyList(), - copyStreams(audios, GROUP_INDEX_AUDIO, streamKeys), - copyStreams(subtitles, GROUP_INDEX_SUBTITLE, streamKeys), - // TODO: Update to retain all closed captions. - /* closedCaptions= */ Collections.emptyList(), + variants, + videos, + audios, + subtitles, + closedCaptions, muxedAudioFormat, muxedCaptionFormats, hasIndependentSegments, variableDefinitions, sessionKeyDrmInitData); } - - /** - * Creates a playlist with a single variant. - * - * @param variantUrl The url of the single variant. - * @return A master playlist with a single variant for the provided url. - */ - public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) { - List variant = - Collections.singletonList(Variant.createMediaPlaylistVariantUrl(Uri.parse(variantUrl))); - return new HlsMasterPlaylist( - /* baseUri= */ "", - /* tags= */ Collections.emptyList(), - variant, - /* videos= */ Collections.emptyList(), - /* audios= */ Collections.emptyList(), - /* subtitles= */ Collections.emptyList(), - /* closedCaptions= */ Collections.emptyList(), - /* muxedAudioFormat= */ null, - /* muxedCaptionFormats= */ null, - /* hasIndependentSegments= */ false, - /* variableDefinitions= */ Collections.emptyMap(), - /* sessionKeyDrmInitData= */ Collections.emptyList()); - } - - private static List getMediaPlaylistUrls( - List variants, - List videos, - List audios, - List subtitles, - List closedCaptions) { - ArrayList mediaPlaylistUrls = new ArrayList<>(); - for (int i = 0; i < variants.size(); i++) { - Uri uri = variants.get(i).url; - if (!mediaPlaylistUrls.contains(uri)) { - mediaPlaylistUrls.add(uri); - } - } - addMediaPlaylistUrls(videos, mediaPlaylistUrls); - addMediaPlaylistUrls(audios, mediaPlaylistUrls); - addMediaPlaylistUrls(subtitles, mediaPlaylistUrls); - addMediaPlaylistUrls(closedCaptions, mediaPlaylistUrls); - return mediaPlaylistUrls; - } - - private static void addMediaPlaylistUrls(List renditions, List out) { - for (int i = 0; i < renditions.size(); i++) { - Uri uri = renditions.get(i).url; - if (uri != null && !out.contains(uri)) { - out.add(uri); - } - } - } - - private static List copyStreams( - List streams, int groupIndex, List streamKeys) { - List copiedStreams = new ArrayList<>(streamKeys.size()); - // TODO: - // 1. When variants with the same URL are not de-duplicated, duplicates must not increment - // trackIndex so as to avoid breaking stream keys that have been persisted for offline. All - // duplicates should be copied if the first variant is copied, or discarded otherwise. - // 2. When renditions with null URLs are permitted, they must not increment trackIndex so as to - // avoid breaking stream keys that have been persisted for offline. All renitions with null - // URLs should be copied. They may become unreachable if all variants that reference them are - // removed, but this is OK. - // 3. Renditions with URLs matching copied variants should always themselves be copied, even if - // the corresponding stream key is omitted. Else we're throwing away information for no gain. - for (int i = 0; i < streams.size(); i++) { - T stream = streams.get(i); - for (int j = 0; j < streamKeys.size(); j++) { - StreamKey streamKey = streamKeys.get(j); - if (streamKey.groupIndex == groupIndex && streamKey.streamIndex == i) { - copiedStreams.add(stream); - break; - } - } - } - return copiedStreams; - } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMultivariantPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMultivariantPlaylist.java new file mode 100644 index 0000000000..ae04015c2a --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMultivariantPlaylist.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2016 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.source.hls.playlist; + +import android.net.Uri; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** Represents an HLS multivariant playlist. */ +// TODO(b/211458101): Make non-final once HlsMasterPlaylist is removed. +public class HlsMultivariantPlaylist extends HlsPlaylist { + + /** Represents an empty multivariant playlist, from which no attributes can be inherited. */ + public static final HlsMultivariantPlaylist EMPTY = + new HlsMultivariantPlaylist( + /* baseUri= */ "", + /* tags= */ Collections.emptyList(), + /* variants= */ Collections.emptyList(), + /* videos= */ Collections.emptyList(), + /* audios= */ Collections.emptyList(), + /* subtitles= */ Collections.emptyList(), + /* closedCaptions= */ Collections.emptyList(), + /* muxedAudioFormat= */ null, + /* muxedCaptionFormats= */ Collections.emptyList(), + /* hasIndependentSegments= */ false, + /* variableDefinitions= */ Collections.emptyMap(), + /* sessionKeyDrmInitData= */ Collections.emptyList()); + + // These constants must not be changed because they are persisted in offline stream keys. + public static final int GROUP_INDEX_VARIANT = 0; + public static final int GROUP_INDEX_AUDIO = 1; + public static final int GROUP_INDEX_SUBTITLE = 2; + + /** A variant (i.e. an #EXT-X-STREAM-INF tag) in a multivariant playlist. */ + public static final class Variant { + + /** The variant's url. */ + public final Uri url; + + /** Format information associated with this variant. */ + public final Format format; + + /** The video rendition group referenced by this variant, or {@code null}. */ + @Nullable public final String videoGroupId; + + /** The audio rendition group referenced by this variant, or {@code null}. */ + @Nullable public final String audioGroupId; + + /** The subtitle rendition group referenced by this variant, or {@code null}. */ + @Nullable public final String subtitleGroupId; + + /** The caption rendition group referenced by this variant, or {@code null}. */ + @Nullable public final String captionGroupId; + + /** + * @param url See {@link #url}. + * @param format See {@link #format}. + * @param videoGroupId See {@link #videoGroupId}. + * @param audioGroupId See {@link #audioGroupId}. + * @param subtitleGroupId See {@link #subtitleGroupId}. + * @param captionGroupId See {@link #captionGroupId}. + */ + public Variant( + Uri url, + Format format, + @Nullable String videoGroupId, + @Nullable String audioGroupId, + @Nullable String subtitleGroupId, + @Nullable String captionGroupId) { + this.url = url; + this.format = format; + this.videoGroupId = videoGroupId; + this.audioGroupId = audioGroupId; + this.subtitleGroupId = subtitleGroupId; + this.captionGroupId = captionGroupId; + } + + /** + * Creates a variant for a given media playlist url. + * + * @param url The media playlist url. + * @return The variant instance. + */ + public static Variant createMediaPlaylistVariantUrl(Uri url) { + Format format = + new Format.Builder().setId("0").setContainerMimeType(MimeTypes.APPLICATION_M3U8).build(); + return new Variant( + url, + format, + /* videoGroupId= */ null, + /* audioGroupId= */ null, + /* subtitleGroupId= */ null, + /* captionGroupId= */ null); + } + + /** Returns a copy of this instance with the given {@link Format}. */ + public Variant copyWithFormat(Format format) { + return new Variant(url, format, videoGroupId, audioGroupId, subtitleGroupId, captionGroupId); + } + } + + /** A rendition (i.e. an #EXT-X-MEDIA tag) in a multivariant playlist. */ + public static final class Rendition { + + /** The rendition's url, or null if the tag does not have a URI attribute. */ + @Nullable public final Uri url; + + /** Format information associated with this rendition. */ + public final Format format; + + /** The group to which this rendition belongs. */ + public final String groupId; + + /** The name of the rendition. */ + public final String name; + + /** + * @param url See {@link #url}. + * @param format See {@link #format}. + * @param groupId See {@link #groupId}. + * @param name See {@link #name}. + */ + public Rendition(@Nullable Uri url, Format format, String groupId, String name) { + this.url = url; + this.format = format; + this.groupId = groupId; + this.name = name; + } + } + + /** All of the media playlist URLs referenced by the playlist. */ + public final List mediaPlaylistUrls; + /** The variants declared by the playlist. */ + public final List variants; + /** The video renditions declared by the playlist. */ + public final List videos; + /** The audio renditions declared by the playlist. */ + public final List audios; + /** The subtitle renditions declared by the playlist. */ + public final List subtitles; + /** The closed caption renditions declared by the playlist. */ + public final List closedCaptions; + + /** + * The format of the audio muxed in the variants. May be null if the playlist does not declare any + * muxed audio. + */ + @Nullable public final Format muxedAudioFormat; + /** + * The format of the closed captions declared by the playlist. May be empty if the playlist + * explicitly declares no captions are available, or null if the playlist does not declare any + * captions information. + */ + @Nullable public final List muxedCaptionFormats; + /** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */ + public final Map variableDefinitions; + /** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */ + public final List sessionKeyDrmInitData; + + /** + * @param baseUri See {@link #baseUri}. + * @param tags See {@link #tags}. + * @param variants See {@link #variants}. + * @param videos See {@link #videos}. + * @param audios See {@link #audios}. + * @param subtitles See {@link #subtitles}. + * @param closedCaptions See {@link #closedCaptions}. + * @param muxedAudioFormat See {@link #muxedAudioFormat}. + * @param muxedCaptionFormats See {@link #muxedCaptionFormats}. + * @param hasIndependentSegments See {@link #hasIndependentSegments}. + * @param variableDefinitions See {@link #variableDefinitions}. + * @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}. + */ + public HlsMultivariantPlaylist( + String baseUri, + List tags, + List variants, + List videos, + List audios, + List subtitles, + List closedCaptions, + @Nullable Format muxedAudioFormat, + @Nullable List muxedCaptionFormats, + boolean hasIndependentSegments, + Map variableDefinitions, + List sessionKeyDrmInitData) { + super(baseUri, tags, hasIndependentSegments); + this.mediaPlaylistUrls = + Collections.unmodifiableList( + getMediaPlaylistUrls(variants, videos, audios, subtitles, closedCaptions)); + this.variants = Collections.unmodifiableList(variants); + this.videos = Collections.unmodifiableList(videos); + this.audios = Collections.unmodifiableList(audios); + this.subtitles = Collections.unmodifiableList(subtitles); + this.closedCaptions = Collections.unmodifiableList(closedCaptions); + this.muxedAudioFormat = muxedAudioFormat; + this.muxedCaptionFormats = + muxedCaptionFormats != null ? Collections.unmodifiableList(muxedCaptionFormats) : null; + this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions); + this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData); + } + + @Override + public HlsMultivariantPlaylist copy(List streamKeys) { + return new HlsMultivariantPlaylist( + baseUri, + tags, + copyStreams(variants, GROUP_INDEX_VARIANT, streamKeys), + // TODO: Allow stream keys to specify video renditions to be retained. + /* videos= */ Collections.emptyList(), + copyStreams(audios, GROUP_INDEX_AUDIO, streamKeys), + copyStreams(subtitles, GROUP_INDEX_SUBTITLE, streamKeys), + // TODO: Update to retain all closed captions. + /* closedCaptions= */ Collections.emptyList(), + muxedAudioFormat, + muxedCaptionFormats, + hasIndependentSegments, + variableDefinitions, + sessionKeyDrmInitData); + } + + /** + * Creates a playlist with a single variant. + * + * @param variantUrl The url of the single variant. + * @return A multivariant playlist with a single variant for the provided url. + */ + public static HlsMultivariantPlaylist createSingleVariantMultivariantPlaylist(String variantUrl) { + List variant = + Collections.singletonList(Variant.createMediaPlaylistVariantUrl(Uri.parse(variantUrl))); + return new HlsMultivariantPlaylist( + /* baseUri= */ "", + /* tags= */ Collections.emptyList(), + variant, + /* videos= */ Collections.emptyList(), + /* audios= */ Collections.emptyList(), + /* subtitles= */ Collections.emptyList(), + /* closedCaptions= */ Collections.emptyList(), + /* muxedAudioFormat= */ null, + /* muxedCaptionFormats= */ null, + /* hasIndependentSegments= */ false, + /* variableDefinitions= */ Collections.emptyMap(), + /* sessionKeyDrmInitData= */ Collections.emptyList()); + } + + private static List getMediaPlaylistUrls( + List variants, + List videos, + List audios, + List subtitles, + List closedCaptions) { + ArrayList mediaPlaylistUrls = new ArrayList<>(); + for (int i = 0; i < variants.size(); i++) { + Uri uri = variants.get(i).url; + if (!mediaPlaylistUrls.contains(uri)) { + mediaPlaylistUrls.add(uri); + } + } + addMediaPlaylistUrls(videos, mediaPlaylistUrls); + addMediaPlaylistUrls(audios, mediaPlaylistUrls); + addMediaPlaylistUrls(subtitles, mediaPlaylistUrls); + addMediaPlaylistUrls(closedCaptions, mediaPlaylistUrls); + return mediaPlaylistUrls; + } + + private static void addMediaPlaylistUrls(List renditions, List out) { + for (int i = 0; i < renditions.size(); i++) { + Uri uri = renditions.get(i).url; + if (uri != null && !out.contains(uri)) { + out.add(uri); + } + } + } + + private static List copyStreams( + List streams, int groupIndex, List streamKeys) { + List copiedStreams = new ArrayList<>(streamKeys.size()); + // TODO: + // 1. When variants with the same URL are not de-duplicated, duplicates must not increment + // trackIndex so as to avoid breaking stream keys that have been persisted for offline. All + // duplicates should be copied if the first variant is copied, or discarded otherwise. + // 2. When renditions with null URLs are permitted, they must not increment trackIndex so as to + // avoid breaking stream keys that have been persisted for offline. All renitions with null + // URLs should be copied. They may become unreachable if all variants that reference them are + // removed, but this is OK. + // 3. Renditions with URLs matching copied variants should always themselves be copied, even if + // the corresponding stream key is omitted. Else we're throwing away information for no gain. + for (int i = 0; i < streams.size(); i++) { + T stream = streams.get(i); + for (int j = 0; j < streamKeys.size(); j++) { + StreamKey streamKey = streamKeys.get(j); + if (streamKey.groupIndex == groupIndex && streamKey.streamIndex == i) { + copiedStreams.add(stream); + break; + } + } + } + return copiedStreams; + } +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 4a445c54e4..3a2680277f 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -32,11 +32,11 @@ import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry; import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry.VariantInfo; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Part; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.RenditionReport; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; +import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist.Rendition; +import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist.Variant; import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -223,28 +223,30 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser> urlToVariantInfos = new HashMap<>(); HashMap variableDefinitions = new HashMap<>(); ArrayList variants = new ArrayList<>(); @@ -578,7 +580,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variableDefinitions = new HashMap<>(); @@ -748,11 +750,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser createPlaylistParser( - HlsMasterPlaylist masterPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist); + HlsMultivariantPlaylist multivariantPlaylist, + @Nullable HlsMediaPlaylist previousMediaPlaylist); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 8396c9c409..06f369d2a2 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -29,8 +29,8 @@ import java.io.IOException; *

          The playlist tracker is responsible for exposing the seeking window, which is defined by the * segments that one of the playlists exposes. This playlist is called primary and needs to be * periodically refreshed in the case of live streams. Note that the primary playlist is one of the - * media playlists while the master playlist is an optional kind of playlist defined by the HLS - * specification (RFC 8216). + * media playlists while the multivariant playlist is an optional kind of playlist defined by the + * HLS specification (RFC 8216). * *

          Playlist loads might encounter errors. The tracker may choose to exclude them to ensure a * primary playlist is always available. @@ -120,8 +120,8 @@ public interface HlsPlaylistTracker { *

          Must be called from the playback thread. A tracker may be restarted after a {@link #stop()} * call. * - * @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master - * playlist. + * @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a + * multivariant playlist. * @param eventDispatcher A dispatcher to notify of events. * @param primaryPlaylistListener A callback for the primary playlist change events. */ @@ -152,15 +152,15 @@ public interface HlsPlaylistTracker { void removeListener(PlaylistEventListener listener); /** - * Returns the master playlist. + * Returns the multivariant playlist. * - *

          If the uri passed to {@link #start} points to a media playlist, an {@link HlsMasterPlaylist} - * with a single variant for said media playlist is returned. + *

          If the uri passed to {@link #start} points to a media playlist, an {@link + * HlsMultivariantPlaylist} with a single variant for said media playlist is returned. * - * @return The master playlist. Null if the initial playlist has yet to be loaded. + * @return The multivariant playlist. Null if the initial playlist has yet to be loaded. */ @Nullable - HlsMasterPlaylist getMasterPlaylist(); + HlsMultivariantPlaylist getMultivariantPlaylist(); /** * Returns the most recent snapshot available of the playlist referenced by the provided {@link @@ -192,8 +192,8 @@ public interface HlsPlaylistTracker { boolean isSnapshotValid(Uri url); /** - * If the tracker is having trouble refreshing the master playlist or the primary playlist, this - * method throws the underlying error. Otherwise, does nothing. + * If the tracker is having trouble refreshing the multivariant playlist or the primary playlist, + * this method throws the underlying error. Otherwise, does nothing. * * @throws IOException The underlying error. */ diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java index 39f59e7a33..317ed23e1a 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java @@ -28,9 +28,9 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant; +import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist.Rendition; +import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist.Variant; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.testutil.MediaPeriodAsserts; @@ -51,9 +51,9 @@ import org.junit.runner.RunWith; public final class HlsMediaPeriodTest { @Test - public void getSteamKeys_isCompatibleWithHlsMasterPlaylistFilter() { - HlsMasterPlaylist testMasterPlaylist = - createMasterPlaylist( + public void getSteamKeys_isCompatibleWithHlsMultivariantPlaylistFilter() { + HlsMultivariantPlaylist testMultivariantPlaylist = + createMultivariantPlaylist( /* variants= */ Arrays.asList( createAudioOnlyVariant(/* peakBitrate= */ 10000), createMuxedVideoAudioVariant(/* peakBitrate= */ 200000), @@ -76,7 +76,8 @@ public final class HlsMediaPeriodTest { HlsDataSourceFactory mockDataSourceFactory = mock(HlsDataSourceFactory.class); when(mockDataSourceFactory.createDataSource(anyInt())).thenReturn(mock(DataSource.class)); HlsPlaylistTracker mockPlaylistTracker = mock(HlsPlaylistTracker.class); - when(mockPlaylistTracker.getMasterPlaylist()).thenReturn((HlsMasterPlaylist) playlist); + when(mockPlaylistTracker.getMultivariantPlaylist()) + .thenReturn((HlsMultivariantPlaylist) playlist); MediaPeriodId mediaPeriodId = new MediaPeriodId(/* periodUid= */ new Object()); return new HlsMediaPeriod( mock(HlsExtractorFactory.class), @@ -98,16 +99,16 @@ public final class HlsMediaPeriodTest { }; MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration( - mediaPeriodFactory, testMasterPlaylist); + mediaPeriodFactory, testMultivariantPlaylist); } - private static HlsMasterPlaylist createMasterPlaylist( + private static HlsMultivariantPlaylist createMultivariantPlaylist( List variants, List audios, List subtitles, Format muxedAudioFormat, List muxedCaptionFormats) { - return new HlsMasterPlaylist( + return new HlsMultivariantPlaylist( "http://baseUri", /* tags= */ Collections.emptyList(), variants, diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java index 9215dd31f0..62645d0eca 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java @@ -20,11 +20,11 @@ import com.google.common.base.Charsets; /** Data for HLS downloading tests. */ /* package */ interface HlsDownloadTestData { - String MASTER_PLAYLIST_URI = "test.m3u8"; - int MASTER_MEDIA_PLAYLIST_1_INDEX = 0; - int MASTER_MEDIA_PLAYLIST_2_INDEX = 1; - int MASTER_MEDIA_PLAYLIST_3_INDEX = 2; - int MASTER_MEDIA_PLAYLIST_0_INDEX = 3; + String MULTIVARIANT_PLAYLIST_URI = "test.m3u8"; + int MULTIVARIANT_MEDIA_PLAYLIST_1_INDEX = 0; + int MULTIVARIANT_MEDIA_PLAYLIST_2_INDEX = 1; + int MULTIVARIANT_MEDIA_PLAYLIST_3_INDEX = 2; + int MULTIVARIANT_MEDIA_PLAYLIST_0_INDEX = 3; String MEDIA_PLAYLIST_0_DIR = "gear0/"; String MEDIA_PLAYLIST_0_URI = MEDIA_PLAYLIST_0_DIR + "prog_index.m3u8"; @@ -35,7 +35,7 @@ import com.google.common.base.Charsets; String MEDIA_PLAYLIST_3_DIR = "gear3/"; String MEDIA_PLAYLIST_3_URI = MEDIA_PLAYLIST_3_DIR + "prog_index.m3u8"; - byte[] MASTER_PLAYLIST_DATA = + byte[] MULTIVARIANT_PLAYLIST_DATA = ("#EXTM3U\n" + "#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\"mp4a.40.2, avc1.4d4015\"\n" + MEDIA_PLAYLIST_1_URI diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java index b8ff3680bf..e4e3ce2fac 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java @@ -17,10 +17,6 @@ package com.google.android.exoplayer2.source.hls.offline; import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_DATA; import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_URI; -import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_MEDIA_PLAYLIST_1_INDEX; -import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_MEDIA_PLAYLIST_2_INDEX; -import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_DATA; -import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_URI; import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_DIR; import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_URI; import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_1_DIR; @@ -30,6 +26,10 @@ import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestDa import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_DIR; import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_URI; import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_DATA; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MULTIVARIANT_MEDIA_PLAYLIST_1_INDEX; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MULTIVARIANT_MEDIA_PLAYLIST_2_INDEX; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MULTIVARIANT_PLAYLIST_DATA; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MULTIVARIANT_PLAYLIST_URI; import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; import static com.google.common.truth.Truth.assertThat; @@ -43,7 +43,7 @@ import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.Downloader; import com.google.android.exoplayer2.offline.DownloaderFactory; import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist; import com.google.android.exoplayer2.testutil.CacheAsserts; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; @@ -83,7 +83,7 @@ public class HlsDownloaderTest { progressListener = new ProgressListener(); fakeDataSet = new FakeDataSet() - .setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA) + .setData(MULTIVARIANT_PLAYLIST_URI, MULTIVARIANT_PLAYLIST_DATA) .setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA) .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10) .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", 11) @@ -122,7 +122,7 @@ public class HlsDownloaderTest { @Test public void counterMethods() throws Exception { HlsDownloader downloader = - getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX)); + getHlsDownloader(MULTIVARIANT_PLAYLIST_URI, getKeys(MULTIVARIANT_MEDIA_PLAYLIST_1_INDEX)); downloader.download(progressListener); progressListener.assertBytesDownloaded(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12); @@ -131,14 +131,14 @@ public class HlsDownloaderTest { @Test public void downloadRepresentation() throws Exception { HlsDownloader downloader = - getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX)); + getHlsDownloader(MULTIVARIANT_PLAYLIST_URI, getKeys(MULTIVARIANT_MEDIA_PLAYLIST_1_INDEX)); downloader.download(progressListener); assertCachedData( cache, new CacheAsserts.RequestSet(fakeDataSet) .subset( - MASTER_PLAYLIST_URI, + MULTIVARIANT_PLAYLIST_URI, MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", @@ -149,8 +149,8 @@ public class HlsDownloaderTest { public void downloadMultipleRepresentations() throws Exception { HlsDownloader downloader = getHlsDownloader( - MASTER_PLAYLIST_URI, - getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX)); + MULTIVARIANT_PLAYLIST_URI, + getKeys(MULTIVARIANT_MEDIA_PLAYLIST_1_INDEX, MULTIVARIANT_MEDIA_PLAYLIST_2_INDEX)); downloader.download(progressListener); assertCachedData(cache, fakeDataSet); @@ -169,7 +169,7 @@ public class HlsDownloaderTest { .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence1.ts", 14) .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence2.ts", 15); - HlsDownloader downloader = getHlsDownloader(MASTER_PLAYLIST_URI, getKeys()); + HlsDownloader downloader = getHlsDownloader(MULTIVARIANT_PLAYLIST_URI, getKeys()); downloader.download(progressListener); assertCachedData(cache, fakeDataSet); @@ -179,8 +179,8 @@ public class HlsDownloaderTest { public void remove() throws Exception { HlsDownloader downloader = getHlsDownloader( - MASTER_PLAYLIST_URI, - getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX)); + MULTIVARIANT_PLAYLIST_URI, + getKeys(MULTIVARIANT_MEDIA_PLAYLIST_1_INDEX, MULTIVARIANT_MEDIA_PLAYLIST_2_INDEX)); downloader.download(progressListener); downloader.remove(); @@ -231,7 +231,7 @@ public class HlsDownloaderTest { private static ArrayList getKeys(int... variantIndices) { ArrayList streamKeys = new ArrayList<>(); for (int variantIndex : variantIndices) { - streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, variantIndex)); + streamKeys.add(new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, variantIndex)); } return streamKeys; } diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTrackerTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTrackerTest.java index 80060b15f8..4580bb4d21 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTrackerTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTrackerTest.java @@ -44,9 +44,10 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class DefaultHlsPlaylistTrackerTest { - private static final String SAMPLE_M3U8_LIVE_MASTER = "media/m3u8/live_low_latency_master"; - private static final String SAMPLE_M3U8_LIVE_MASTER_MEDIA_URI_WITH_PARAM = - "media/m3u8/live_low_latency_master_media_uri_with_param"; + private static final String SAMPLE_M3U8_LIVE_MULTIVARIANT = + "media/m3u8/live_low_latency_multivariant"; + private static final String SAMPLE_M3U8_LIVE_MULTIVARIANT_MEDIA_URI_WITH_PARAM = + "media/m3u8/live_low_latency_multivariant_media_uri_with_param"; private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL = "media/m3u8/live_low_latency_media_can_skip_until"; private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_FULL_RELOAD_AFTER_ERROR = @@ -110,15 +111,15 @@ public class DefaultHlsPlaylistTrackerTest { throws IOException, TimeoutException, InterruptedException { List httpUrls = enqueueWebServerResponses( - new String[] {"master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8"}, - getMockResponse(SAMPLE_M3U8_LIVE_MASTER), + new String[] {"multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8"}, + getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP_NEXT)); List mediaPlaylists = runPlaylistTrackerAndCollectMediaPlaylists( new DefaultHttpDataSource.Factory(), - Uri.parse(mockWebServer.url("/master.m3u8").toString()), + Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()), /* awaitedMediaPlaylistCount= */ 2); assertRequestUrlsCalled(httpUrls); @@ -141,16 +142,16 @@ public class DefaultHlsPlaylistTrackerTest { List httpUrls = enqueueWebServerResponses( new String[] { - "/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=YES" + "/multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=YES" }, - getMockResponse(SAMPLE_M3U8_LIVE_MASTER), + getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED)); List mediaPlaylists = runPlaylistTrackerAndCollectMediaPlaylists( new DefaultHttpDataSource.Factory(), - Uri.parse(mockWebServer.url("/master.m3u8").toString()), + Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()), /* awaitedMediaPlaylistCount= */ 2); assertRequestUrlsCalled(httpUrls); @@ -175,12 +176,12 @@ public class DefaultHlsPlaylistTrackerTest { List httpUrls = enqueueWebServerResponses( new String[] { - "/master.m3u8", + "/multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=YES", "/media0/playlist.m3u8" }, - getMockResponse(SAMPLE_M3U8_LIVE_MASTER), + getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED_MEDIA_SEQUENCE_NO_OVERLAPPING), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_FULL_RELOAD_AFTER_ERROR)); @@ -188,7 +189,7 @@ public class DefaultHlsPlaylistTrackerTest { List mediaPlaylists = runPlaylistTrackerAndCollectMediaPlaylists( new DefaultHttpDataSource.Factory(), - Uri.parse(mockWebServer.url("/master.m3u8").toString()), + Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()), /* awaitedMediaPlaylistCount= */ 2); assertRequestUrlsCalled(httpUrls); @@ -206,16 +207,16 @@ public class DefaultHlsPlaylistTrackerTest { List httpUrls = enqueueWebServerResponses( new String[] { - "/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=v2" + "/multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=v2" }, - getMockResponse(SAMPLE_M3U8_LIVE_MASTER), + getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_DATERANGES), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED)); List mediaPlaylists = runPlaylistTrackerAndCollectMediaPlaylists( new DefaultHttpDataSource.Factory(), - Uri.parse(mockWebServer.url("/master.m3u8").toString()), + Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()), /* awaitedMediaPlaylistCount= */ 2); assertRequestUrlsCalled(httpUrls); @@ -230,18 +231,18 @@ public class DefaultHlsPlaylistTrackerTest { List httpUrls = enqueueWebServerResponses( new String[] { - "/master.m3u8", + "/multivariant.m3u8", "/media0/playlist.m3u8?param1=1¶m2=2", "/media0/playlist.m3u8?param1=1¶m2=2&_HLS_skip=YES" }, - getMockResponse(SAMPLE_M3U8_LIVE_MASTER_MEDIA_URI_WITH_PARAM), + getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT_MEDIA_URI_WITH_PARAM), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED)); List mediaPlaylists = runPlaylistTrackerAndCollectMediaPlaylists( new DefaultHttpDataSource.Factory(), - Uri.parse(mockWebServer.url("/master.m3u8").toString()), + Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()), /* awaitedMediaPlaylistCount= */ 2); assertRequestUrlsCalled(httpUrls); @@ -257,16 +258,16 @@ public class DefaultHlsPlaylistTrackerTest { List httpUrls = enqueueWebServerResponses( new String[] { - "/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_msn=14" + "/multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_msn=14" }, - getMockResponse(SAMPLE_M3U8_LIVE_MASTER), + getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_NEXT)); List mediaPlaylists = runPlaylistTrackerAndCollectMediaPlaylists( new DefaultHttpDataSource.Factory(), - Uri.parse(mockWebServer.url("/master.m3u8").toString()), + Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()), /* awaitedMediaPlaylistCount= */ 2); assertRequestUrlsCalled(httpUrls); @@ -281,18 +282,18 @@ public class DefaultHlsPlaylistTrackerTest { List httpUrls = enqueueWebServerResponses( new String[] { - "/master.m3u8", + "/multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_msn=14&_HLS_part=1" }, - getMockResponse(SAMPLE_M3U8_LIVE_MASTER), + getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_NEXT)); List mediaPlaylists = runPlaylistTrackerAndCollectMediaPlaylists( new DefaultHttpDataSource.Factory(), - Uri.parse(mockWebServer.url("/master.m3u8").toString()), + Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()), /* awaitedMediaPlaylistCount= */ 2); assertRequestUrlsCalled(httpUrls); @@ -310,18 +311,18 @@ public class DefaultHlsPlaylistTrackerTest { List httpUrls = enqueueWebServerResponses( new String[] { - "/master.m3u8", + "/multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_msn=14&_HLS_part=0" }, - getMockResponse(SAMPLE_M3U8_LIVE_MASTER), + getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_NEXT)); List mediaPlaylists = runPlaylistTrackerAndCollectMediaPlaylists( new DefaultHttpDataSource.Factory(), - Uri.parse(mockWebServer.url("/master.m3u8").toString()), + Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()), /* awaitedMediaPlaylistCount= */ 2); assertRequestUrlsCalled(httpUrls); @@ -339,11 +340,11 @@ public class DefaultHlsPlaylistTrackerTest { List httpUrls = enqueueWebServerResponses( new String[] { - "/master.m3u8", + "/multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_msn=14&_HLS_part=0" }, - getMockResponse(SAMPLE_M3U8_LIVE_MASTER), + getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT), getMockResponse( SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_PRELOAD), getMockResponse( @@ -352,7 +353,7 @@ public class DefaultHlsPlaylistTrackerTest { List mediaPlaylists = runPlaylistTrackerAndCollectMediaPlaylists( new DefaultHttpDataSource.Factory(), - Uri.parse(mockWebServer.url("/master.m3u8").toString()), + Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()), /* awaitedMediaPlaylistCount= */ 2); assertRequestUrlsCalled(httpUrls); @@ -370,13 +371,13 @@ public class DefaultHlsPlaylistTrackerTest { List httpUrls = enqueueWebServerResponses( new String[] { - "/master.m3u8", + "/multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_msn=16&_HLS_skip=YES", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_msn=17&_HLS_skip=YES" }, - getMockResponse(SAMPLE_M3U8_LIVE_MASTER), + getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD), new MockResponse().setResponseCode(400), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD_NEXT), @@ -385,7 +386,7 @@ public class DefaultHlsPlaylistTrackerTest { List mediaPlaylists = runPlaylistTrackerAndCollectMediaPlaylists( /* dataSourceFactory= */ new DefaultHttpDataSource.Factory(), - Uri.parse(mockWebServer.url("/master.m3u8").toString()), + Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()), /* awaitedMediaPlaylistCount= */ 3); assertRequestUrlsCalled(httpUrls); @@ -415,7 +416,9 @@ public class DefaultHlsPlaylistTrackerTest { } private static List runPlaylistTrackerAndCollectMediaPlaylists( - DataSource.Factory dataSourceFactory, Uri masterPlaylistUri, int awaitedMediaPlaylistCount) + DataSource.Factory dataSourceFactory, + Uri multivariantPlaylistUri, + int awaitedMediaPlaylistCount) throws TimeoutException { DefaultHlsPlaylistTracker defaultHlsPlaylistTracker = @@ -427,7 +430,7 @@ public class DefaultHlsPlaylistTrackerTest { List mediaPlaylists = new ArrayList<>(); AtomicInteger playlistCounter = new AtomicInteger(); defaultHlsPlaylistTracker.start( - masterPlaylistUri, + multivariantPlaylistUri, new MediaSourceEventListener.EventDispatcher(), mediaPlaylist -> { mediaPlaylists.add(mediaPlaylist); diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index 6b388a50a9..6fa94af3a3 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -392,7 +392,7 @@ public class HlsMediaPlaylistParserTest { HlsMediaPlaylist playlist = (HlsMediaPlaylist) - new HlsPlaylistParser(HlsMasterPlaylist.EMPTY, previousPlaylist) + new HlsPlaylistParser(HlsMultivariantPlaylist.EMPTY, previousPlaylist) .parse(playlistUri, inputStream); assertThat(playlist.segments).hasSize(3); @@ -446,7 +446,7 @@ public class HlsMediaPlaylistParserTest { HlsMediaPlaylist playlist = (HlsMediaPlaylist) - new HlsPlaylistParser(HlsMasterPlaylist.EMPTY, previousPlaylist) + new HlsPlaylistParser(HlsMultivariantPlaylist.EMPTY, previousPlaylist) .parse(playlistUri, inputStream); assertThat(playlist.segments).hasSize(2); @@ -1363,7 +1363,7 @@ public class HlsMediaPlaylistParserTest { } @Test - public void masterPlaylistAttributeInheritance() throws IOException { + public void multivariantPlaylistAttributeInheritance() throws IOException { Uri playlistUri = Uri.parse("https://example.com/test3.m3u8"); String playlistString = "#EXTM3U\n" @@ -1386,8 +1386,8 @@ public class HlsMediaPlaylistParserTest { assertThat(standalonePlaylist.hasIndependentSegments).isFalse(); inputStream.reset(); - HlsMasterPlaylist masterPlaylist = - new HlsMasterPlaylist( + HlsMultivariantPlaylist multivariantPlaylist = + new HlsMultivariantPlaylist( /* baseUri= */ "https://example.com/", /* tags= */ Collections.emptyList(), /* variants= */ Collections.emptyList(), @@ -1402,7 +1402,7 @@ public class HlsMediaPlaylistParserTest { /* sessionKeyDrmInitData= */ Collections.emptyList()); HlsMediaPlaylist playlistWithInheritance = (HlsMediaPlaylist) - new HlsPlaylistParser(masterPlaylist, /* previousMediaPlaylist= */ null) + new HlsPlaylistParser(multivariantPlaylist, /* previousMediaPlaylist= */ null) .parse(playlistUri, inputStream); assertThat(playlistWithInheritance.hasIndependentSegments).isTrue(); } @@ -1450,8 +1450,8 @@ public class HlsMediaPlaylistParserTest { InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HashMap variableDefinitions = new HashMap<>(); variableDefinitions.put("imported_base", "long_path"); - HlsMasterPlaylist masterPlaylist = - new HlsMasterPlaylist( + HlsMultivariantPlaylist multivariantPlaylist = + new HlsMultivariantPlaylist( /* baseUri= */ "", /* tags= */ Collections.emptyList(), /* variants= */ Collections.emptyList(), @@ -1466,7 +1466,7 @@ public class HlsMediaPlaylistParserTest { /* sessionKeyDrmInitData= */ Collections.emptyList()); HlsMediaPlaylist playlist = (HlsMediaPlaylist) - new HlsPlaylistParser(masterPlaylist, /* previousMediaPlaylist= */ null) + new HlsPlaylistParser(multivariantPlaylist, /* previousMediaPlaylist= */ null) .parse(playlistUri, inputStream); for (int i = 1; i <= 4; i++) { assertThat(playlist.segments.get(i - 1).url).isEqualTo("long_path" + i + ".ts"); diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMultivariantPlaylistParserTest.java similarity index 82% rename from library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java rename to library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMultivariantPlaylistParserTest.java index dff755a443..133457ea27 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMultivariantPlaylistParserTest.java @@ -25,7 +25,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant; +import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist.Variant; import com.google.android.exoplayer2.util.MimeTypes; import com.google.common.base.Charsets; import java.io.ByteArrayInputStream; @@ -36,9 +36,9 @@ import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; -/** Test for {@link HlsMasterPlaylist}. */ +/** Test for {@link HlsMultivariantPlaylist}. */ @RunWith(AndroidJUnit4.class) -public class HlsMasterPlaylistParserTest { +public class HlsMultivariantPlaylistParserTest { private static final String PLAYLIST_URI = "https://example.com/test.m3u8"; @@ -233,12 +233,13 @@ public class HlsMasterPlaylistParserTest { + "#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1313400,RESOLUTION=1920x1080,CODECS=\"avc1.640028\",URI=\"iframe_1313400/index.m3u8\"\n"; @Test - public void parseMasterPlaylist_withSimple_success() throws IOException { - HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); + public void parseMultivariantPlaylist_withSimple_success() throws IOException { + HlsMultivariantPlaylist multivariantPlaylist = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); - List variants = masterPlaylist.variants; + List variants = multivariantPlaylist.variants; assertThat(variants).hasSize(5); - assertThat(masterPlaylist.muxedCaptionFormats).isNull(); + assertThat(multivariantPlaylist.muxedCaptionFormats).isNull(); assertThat(variants.get(0).format.bitrate).isEqualTo(1280000); assertThat(variants.get(0).format.codecs).isEqualTo("mp4a.40.2,avc1.66.30"); @@ -274,20 +275,20 @@ public class HlsMasterPlaylistParserTest { } @Test - public void parseMasterPlaylist_withAverageBandwidth_success() throws IOException { - HlsMasterPlaylist masterPlaylist = - parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AVG_BANDWIDTH); + public void parseMultivariantPlaylist_withAverageBandwidth_success() throws IOException { + HlsMultivariantPlaylist multivariantPlaylist = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AVG_BANDWIDTH); - List variants = masterPlaylist.variants; + List variants = multivariantPlaylist.variants; assertThat(variants.get(0).format.bitrate).isEqualTo(1280000); assertThat(variants.get(1).format.bitrate).isEqualTo(1280000); } @Test - public void parseMasterPlaylist_withInvalidHeader_throwsException() throws IOException { + public void parseMultivariantPlaylist_withInvalidHeader_throwsException() throws IOException { try { - parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); fail("Expected exception not thrown."); } catch (ParserException e) { // Expected due to invalid header. @@ -295,8 +296,8 @@ public class HlsMasterPlaylistParserTest { } @Test - public void parseMasterPlaylist_withClosedCaption_success() throws IOException { - HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC); + public void parseMultivariantPlaylist_withClosedCaption_success() throws IOException { + HlsMultivariantPlaylist playlist = parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC); assertThat(playlist.muxedCaptionFormats).hasSize(1); Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0); assertThat(closedCaptionFormat.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA708); @@ -305,10 +306,10 @@ public class HlsMasterPlaylistParserTest { } @Test - public void parseMasterPlaylist_withChannelsAttribute_success() throws IOException { - HlsMasterPlaylist playlist = - parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CHANNELS_ATTRIBUTE); - List audios = playlist.audios; + public void parseMultivariantPlaylist_withChannelsAttribute_success() throws IOException { + HlsMultivariantPlaylist playlist = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CHANNELS_ATTRIBUTE); + List audios = playlist.audios; assertThat(audios).hasSize(3); assertThat(audios.get(0).format.channelCount).isEqualTo(6); assertThat(audios.get(1).format.channelCount).isEqualTo(2); @@ -316,14 +317,15 @@ public class HlsMasterPlaylistParserTest { } @Test - public void parseMasterPlaylist_withoutClosedCaption_success() throws IOException { - HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC); + public void parseMultivariantPlaylist_withoutClosedCaption_success() throws IOException { + HlsMultivariantPlaylist playlist = parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC); assertThat(playlist.muxedCaptionFormats).isEmpty(); } @Test - public void parseMasterPlaylist_withAudio_codecPropagated() throws IOException { - HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG); + public void parseMultivariantPlaylist_withAudio_codecPropagated() throws IOException { + HlsMultivariantPlaylist playlist = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG); Format firstAudioFormat = playlist.audios.get(0).format; assertThat(firstAudioFormat.codecs).isEqualTo("mp4a.40.2"); @@ -335,8 +337,9 @@ public class HlsMasterPlaylistParserTest { } @Test - public void parseMasterPlaylist_withAudio_audioIdPropagated() throws IOException { - HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG); + public void parseMultivariantPlaylist_withAudio_audioIdPropagated() throws IOException { + HlsMultivariantPlaylist playlist = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG); Format firstAudioFormat = playlist.audios.get(0).format; assertThat(firstAudioFormat.id).isEqualTo("aud1:English"); @@ -346,16 +349,17 @@ public class HlsMasterPlaylistParserTest { } @Test - public void parseMasterPlaylist_withCc_cCIdPropagated() throws IOException { - HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC); + public void parseMultivariantPlaylist_withCc_cCIdPropagated() throws IOException { + HlsMultivariantPlaylist playlist = parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC); Format firstTextFormat = playlist.muxedCaptionFormats.get(0); assertThat(firstTextFormat.id).isEqualTo("cc1:Eng"); } @Test - public void parseMasterPlaylist_withSubtitles_subtitlesIdPropagated() throws IOException { - HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_SUBTITLES); + public void parseMultivariantPlaylist_withSubtitles_subtitlesIdPropagated() throws IOException { + HlsMultivariantPlaylist playlist = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_SUBTITLES); Format firstTextFormat = playlist.subtitles.get(0).format; assertThat(firstTextFormat.id).isEqualTo("sub1:Eng"); @@ -363,39 +367,40 @@ public class HlsMasterPlaylistParserTest { } @Test - public void parseMasterPlaylist_subtitlesWithoutUri_skipsSubtitles() throws IOException { - HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_SUBTITLES_NO_URI); + public void parseMultivariantPlaylist_subtitlesWithoutUri_skipsSubtitles() throws IOException { + HlsMultivariantPlaylist playlist = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_SUBTITLES_NO_URI); assertThat(playlist.subtitles).isEmpty(); } @Test - public void parseMasterPlaylist_withIndependentSegments_hasNoIndenpendentSegments() + public void parseMultivariantPlaylist_withIndependentSegments_hasNoIndenpendentSegments() throws IOException { - HlsMasterPlaylist playlistWithIndependentSegments = - parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INDEPENDENT_SEGMENTS); + HlsMultivariantPlaylist playlistWithIndependentSegments = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INDEPENDENT_SEGMENTS); assertThat(playlistWithIndependentSegments.hasIndependentSegments).isTrue(); - HlsMasterPlaylist playlistWithoutIndependentSegments = - parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); + HlsMultivariantPlaylist playlistWithoutIndependentSegments = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); assertThat(playlistWithoutIndependentSegments.hasIndependentSegments).isFalse(); } @Test - public void parseMasterPlaylist_withVariableSubstitution_success() throws IOException { - HlsMasterPlaylist playlistWithSubstitutions = - parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_VARIABLE_SUBSTITUTION); - HlsMasterPlaylist.Variant variant = playlistWithSubstitutions.variants.get(0); + public void parseMultivariantPlaylist_withVariableSubstitution_success() throws IOException { + HlsMultivariantPlaylist playlistWithSubstitutions = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_VARIABLE_SUBSTITUTION); + HlsMultivariantPlaylist.Variant variant = playlistWithSubstitutions.variants.get(0); assertThat(variant.format.codecs).isEqualTo("mp4a.40.5"); assertThat(variant.url) .isEqualTo(Uri.parse("http://example.com/This/{$nested}/reference/shouldnt/work")); } @Test - public void parseMasterPlaylist_withTtmlSubtitle() throws IOException { - HlsMasterPlaylist playlistWithTtmlSubtitle = - parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_TTML_SUBTITLE); - HlsMasterPlaylist.Variant variant = playlistWithTtmlSubtitle.variants.get(0); + public void parseMultivariantPlaylist_withTtmlSubtitle() throws IOException { + HlsMultivariantPlaylist playlistWithTtmlSubtitle = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_TTML_SUBTITLE); + HlsMultivariantPlaylist.Variant variant = playlistWithTtmlSubtitle.variants.get(0); Format firstTextFormat = playlistWithTtmlSubtitle.subtitles.get(0).format; assertThat(firstTextFormat.id).isEqualTo("sub1:English"); assertThat(firstTextFormat.containerMimeType).isEqualTo(MimeTypes.APPLICATION_M3U8); @@ -404,9 +409,9 @@ public class HlsMasterPlaylistParserTest { } @Test - public void parseMasterPlaylist_withMatchingStreamInfUrls_success() throws IOException { - HlsMasterPlaylist playlist = - parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_MATCHING_STREAM_INF_URLS); + public void parseMultivariantPlaylist_withMatchingStreamInfUrls_success() throws IOException { + HlsMultivariantPlaylist playlist = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_MATCHING_STREAM_INF_URLS); assertThat(playlist.variants).hasSize(4); assertThat(playlist.variants.get(0).format.metadata) .isEqualTo( @@ -441,7 +446,8 @@ public class HlsMasterPlaylistParserTest { @Test public void testIFrameVariant() throws IOException { - HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_IFRAME_VARIANTS); + HlsMultivariantPlaylist playlist = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_IFRAME_VARIANTS); assertThat(playlist.variants).hasSize(5); for (int i = 0; i < 4; i++) { assertThat(playlist.variants.get(i).format.roleFlags).isEqualTo(0); @@ -472,11 +478,11 @@ public class HlsMasterPlaylistParserTest { /* captionGroupId= */ "cc1"); } - private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) - throws IOException { + private static HlsMultivariantPlaylist parseMultivariantPlaylist( + String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); ByteArrayInputStream inputStream = new ByteArrayInputStream(playlistString.getBytes(Charsets.UTF_8)); - return (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + return (HlsMultivariantPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); } } diff --git a/testdata/src/test/assets/media/m3u8/live_low_latency_master b/testdata/src/test/assets/media/m3u8/live_low_latency_multivariant similarity index 100% rename from testdata/src/test/assets/media/m3u8/live_low_latency_master rename to testdata/src/test/assets/media/m3u8/live_low_latency_multivariant diff --git a/testdata/src/test/assets/media/m3u8/live_low_latency_master_media_uri_with_param b/testdata/src/test/assets/media/m3u8/live_low_latency_multivariant_media_uri_with_param similarity index 100% rename from testdata/src/test/assets/media/m3u8/live_low_latency_master_media_uri_with_param rename to testdata/src/test/assets/media/m3u8/live_low_latency_multivariant_media_uri_with_param From 7d83c979a6a3ae3e7388de479b7e5c80a65f98db Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 21 Dec 2021 14:27:19 +0000 Subject: [PATCH 36/56] Include role and selection flags when logging a track's Format Inspired by my investigation of Issue: google/ExoPlayer#9797 #minor-release PiperOrigin-RevId: 417609076 --- .../java/com/google/android/exoplayer2/C.java | 2 + .../com/google/android/exoplayer2/Format.java | 73 ++++++++++++++++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/C.java b/library/common/src/main/java/com/google/android/exoplayer2/C.java index b8d2def750..24a0fc2d13 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/C.java @@ -604,6 +604,7 @@ public final class C { flag = true, value = {SELECTION_FLAG_DEFAULT, SELECTION_FLAG_FORCED, SELECTION_FLAG_AUTOSELECT}) public @interface SelectionFlags {} + // LINT.IfChange(selection_flags) /** Indicates that the track should be selected if user preferences do not state otherwise. */ public static final int SELECTION_FLAG_DEFAULT = 1; /** @@ -1069,6 +1070,7 @@ public final class C { ROLE_FLAG_TRICK_PLAY }) public @interface RoleFlags {} + // LINT.IfChange(role_flags) /** Indicates a main track. */ public static final int ROLE_FLAG_MAIN = 1; /** diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Format.java b/library/common/src/main/java/com/google/android/exoplayer2/Format.java index 375ef7a309..6d68e5a919 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Format.java @@ -1301,7 +1301,9 @@ public final class Format implements Bundleable { schemes.add("unknown (" + schemeUuid + ")"); } } - builder.append(", drm=[").append(Joiner.on(',').join(schemes)).append(']'); + builder.append(", drm=["); + Joiner.on(',').appendTo(builder, schemes); + builder.append(']'); } if (format.width != NO_VALUE && format.height != NO_VALUE) { builder.append(", res=").append(format.width).append("x").append(format.height); @@ -1321,8 +1323,73 @@ public final class Format implements Bundleable { if (format.label != null) { builder.append(", label=").append(format.label); } - if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) { - builder.append(", trick-play-track"); + if (format.selectionFlags != 0) { + List selectionFlags = new ArrayList<>(); + // LINT.IfChange(selection_flags) + if ((format.selectionFlags & C.SELECTION_FLAG_AUTOSELECT) != 0) { + selectionFlags.add("auto"); + } + if ((format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0) { + selectionFlags.add("default"); + } + if ((format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0) { + selectionFlags.add("forced"); + } + builder.append(", selectionFlags=["); + Joiner.on(',').appendTo(builder, selectionFlags); + builder.append("]"); + } + if (format.roleFlags != 0) { + // LINT.IfChange(role_flags) + List roleFlags = new ArrayList<>(); + if ((format.roleFlags & C.ROLE_FLAG_MAIN) != 0) { + roleFlags.add("main"); + } + if ((format.roleFlags & C.ROLE_FLAG_ALTERNATE) != 0) { + roleFlags.add("alt"); + } + if ((format.roleFlags & C.ROLE_FLAG_SUPPLEMENTARY) != 0) { + roleFlags.add("supplementary"); + } + if ((format.roleFlags & C.ROLE_FLAG_COMMENTARY) != 0) { + roleFlags.add("commentary"); + } + if ((format.roleFlags & C.ROLE_FLAG_DUB) != 0) { + roleFlags.add("dub"); + } + if ((format.roleFlags & C.ROLE_FLAG_EMERGENCY) != 0) { + roleFlags.add("emergency"); + } + if ((format.roleFlags & C.ROLE_FLAG_CAPTION) != 0) { + roleFlags.add("caption"); + } + if ((format.roleFlags & C.ROLE_FLAG_SUBTITLE) != 0) { + roleFlags.add("subtitle"); + } + if ((format.roleFlags & C.ROLE_FLAG_SIGN) != 0) { + roleFlags.add("sign"); + } + if ((format.roleFlags & C.ROLE_FLAG_DESCRIBES_VIDEO) != 0) { + roleFlags.add("describes-video"); + } + if ((format.roleFlags & C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND) != 0) { + roleFlags.add("describes-music"); + } + if ((format.roleFlags & C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY) != 0) { + roleFlags.add("enhanced-intelligibility"); + } + if ((format.roleFlags & C.ROLE_FLAG_TRANSCRIBES_DIALOG) != 0) { + roleFlags.add("transcribes-dialog"); + } + if ((format.roleFlags & C.ROLE_FLAG_EASY_TO_READ) != 0) { + roleFlags.add("easy-read"); + } + if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) { + roleFlags.add("trick-play"); + } + builder.append(", roleFlags=["); + Joiner.on(',').appendTo(builder, roleFlags); + builder.append("]"); } return builder.toString(); } From bc891273b299f151817100d0fd2ea4545b415796 Mon Sep 17 00:00:00 2001 From: claincly Date: Tue, 21 Dec 2021 14:41:32 +0000 Subject: [PATCH 37/56] Rename MediaCodecAdapterWrapper to Codec. Move static factories into a separate class and make it implement an interface that will let tests customize encoder/decoder creation. PiperOrigin-RevId: 417610825 --- .../transformer/AudioSamplePipeline.java | 16 +- .../android/exoplayer2/transformer/Codec.java | 322 ++++++++++++ .../transformer/DefaultCodecFactory.java | 194 ++++++++ .../transformer/MediaCodecAdapterWrapper.java | 471 ------------------ .../exoplayer2/transformer/Transformer.java | 49 +- .../transformer/TransformerAudioRenderer.java | 13 +- .../transformer/TransformerVideoRenderer.java | 14 +- .../transformer/VideoSamplePipeline.java | 14 +- 8 files changed, 604 insertions(+), 489 deletions(-) create mode 100644 library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Codec.java create mode 100644 library/transformer/src/main/java/com/google/android/exoplayer2/transformer/DefaultCodecFactory.java delete mode 100644 library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MediaCodecAdapterWrapper.java diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java index 7b108d06b3..886f38379e 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java @@ -44,8 +44,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final Format inputFormat; private final Transformation transformation; + private final Codec.EncoderFactory encoderFactory; - private final MediaCodecAdapterWrapper decoder; + private final Codec decoder; private final DecoderInputBuffer decoderInputBuffer; private final SonicAudioProcessor sonicAudioProcessor; @@ -55,7 +56,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final DecoderInputBuffer encoderOutputBuffer; private @MonotonicNonNull AudioFormat encoderInputAudioFormat; - private @MonotonicNonNull MediaCodecAdapterWrapper encoder; + private @MonotonicNonNull Codec encoder; private long nextEncoderInputBufferTimeUs; private long encoderBufferDurationRemainder; @@ -63,10 +64,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private boolean drainingSonicForSpeedChange; private float currentSpeed; - public AudioSamplePipeline(Format inputFormat, Transformation transformation) + public AudioSamplePipeline( + Format inputFormat, + Transformation transformation, + Codec.EncoderFactory encoderFactory, + Codec.DecoderFactory decoderFactory) throws TransformationException { this.inputFormat = inputFormat; this.transformation = transformation; + this.encoderFactory = encoderFactory; decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); encoderInputBuffer = @@ -77,7 +83,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER; speedProvider = new SegmentSpeedProvider(inputFormat); currentSpeed = speedProvider.getSpeed(0); - this.decoder = MediaCodecAdapterWrapper.createForAudioDecoding(inputFormat); + this.decoder = decoderFactory.createForAudioDecoding(inputFormat); } @Override @@ -301,7 +307,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } } encoder = - MediaCodecAdapterWrapper.createForAudioEncoding( + encoderFactory.createForAudioEncoding( new Format.Builder() .setSampleMimeType( transformation.audioMimeType == null diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Codec.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Codec.java new file mode 100644 index 0000000000..7322708008 --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Codec.java @@ -0,0 +1,322 @@ +/* + * Copyright 2021 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.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; + +import android.media.MediaCodec; +import android.media.MediaCodec.BufferInfo; +import android.media.MediaFormat; +import android.view.Surface; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.common.collect.ImmutableList; +import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * A wrapper around {@link MediaCodecAdapter}. + * + *

          Provides a layer of abstraction for callers that need to interact with {@link MediaCodec} + * through {@link MediaCodecAdapter}. This is done by simplifying the calls needed to queue and + * dequeue buffers, removing the need to track buffer indices and codec events. + */ +public final class Codec { + + /** A factory for {@link Codec decoder} instances. */ + public interface DecoderFactory { + + /** A default {@code DecoderFactory} implementation. */ + DecoderFactory DEFAULT = new DefaultCodecFactory(); + + /** + * Returns a {@link Codec} for audio decoding. + * + * @param format The {@link Format} (of the input data) used to determine the underlying {@link + * MediaCodec} and its configuration values. + * @return A configured and started decoder wrapper. + * @throws TransformationException If the underlying codec cannot be created. + */ + Codec createForAudioDecoding(Format format) throws TransformationException; + + /** + * Returns a {@link Codec} for video decoding. + * + * @param format The {@link Format} (of the input data) used to determine the underlying {@link + * MediaCodec} and its configuration values. + * @param surface The {@link Surface} to which the decoder output is rendered. + * @return A configured and started decoder wrapper. + * @throws TransformationException If the underlying codec cannot be created. + */ + Codec createForVideoDecoding(Format format, Surface surface) throws TransformationException; + } + + /** A factory for {@link Codec encoder} instances. */ + public interface EncoderFactory { + + /** A default {@code EncoderFactory} implementation. */ + EncoderFactory DEFAULT = new DefaultCodecFactory(); + + /** + * Returns a {@link Codec} for audio encoding. + * + * @param format The {@link Format} (of the output data) used to determine the underlying {@link + * MediaCodec} and its configuration values. + * @return A configured and started encoder wrapper. + * @throws TransformationException If the underlying codec cannot be created. + */ + Codec createForAudioEncoding(Format format) throws TransformationException; + + /** + * Returns a {@link Codec} for video encoding. + * + * @param format The {@link Format} (of the output data) used to determine the underlying {@link + * MediaCodec} and its configuration values. {@link Format#sampleMimeType}, {@link + * Format#width} and {@link Format#height} must be set to those of the desired output video + * format. {@link Format#rotationDegrees} should be 0. The video should always be in + * landscape orientation. + * @return A configured and started encoder wrapper. + * @throws TransformationException If the underlying codec cannot be created. + */ + Codec createForVideoEncoding(Format format) throws TransformationException; + } + + // MediaCodec decoders always output 16 bit PCM, unless configured to output PCM float. + // https://developer.android.com/reference/android/media/MediaCodec#raw-audio-buffers. + private static final int MEDIA_CODEC_PCM_ENCODING = C.ENCODING_PCM_16BIT; + + private final BufferInfo outputBufferInfo; + private final MediaCodecAdapter mediaCodecAdapter; + + private @MonotonicNonNull Format outputFormat; + @Nullable private ByteBuffer outputBuffer; + + private int inputBufferIndex; + private int outputBufferIndex; + private boolean inputStreamEnded; + private boolean outputStreamEnded; + + /** Creates a {@code Codec} from a configured and started {@link MediaCodecAdapter}. */ + public Codec(MediaCodecAdapter mediaCodecAdapter) { + this.mediaCodecAdapter = mediaCodecAdapter; + outputBufferInfo = new BufferInfo(); + inputBufferIndex = C.INDEX_UNSET; + outputBufferIndex = C.INDEX_UNSET; + } + + /** Returns the input {@link Surface}, or null if the input is not a surface. */ + @Nullable + public Surface getInputSurface() { + return mediaCodecAdapter.getInputSurface(); + } + + /** + * Dequeues a writable input buffer, if available. + * + * @param inputBuffer The buffer where the dequeued buffer data is stored. + * @return Whether an input buffer is ready to be used. + */ + @EnsuresNonNullIf(expression = "#1.data", result = true) + public boolean maybeDequeueInputBuffer(DecoderInputBuffer inputBuffer) { + if (inputStreamEnded) { + return false; + } + if (inputBufferIndex < 0) { + inputBufferIndex = mediaCodecAdapter.dequeueInputBufferIndex(); + if (inputBufferIndex < 0) { + return false; + } + inputBuffer.data = mediaCodecAdapter.getInputBuffer(inputBufferIndex); + inputBuffer.clear(); + } + checkNotNull(inputBuffer.data); + return true; + } + + /** + * Queues an input buffer to the decoder. No buffers may be queued after an {@link + * DecoderInputBuffer#isEndOfStream() end of stream} buffer has been queued. + */ + public void queueInputBuffer(DecoderInputBuffer inputBuffer) { + checkState( + !inputStreamEnded, "Input buffer can not be queued after the input stream has ended."); + + int offset = 0; + int size = 0; + if (inputBuffer.data != null && inputBuffer.data.hasRemaining()) { + offset = inputBuffer.data.position(); + size = inputBuffer.data.remaining(); + } + int flags = 0; + if (inputBuffer.isEndOfStream()) { + inputStreamEnded = true; + flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM; + } + mediaCodecAdapter.queueInputBuffer(inputBufferIndex, offset, size, inputBuffer.timeUs, flags); + inputBufferIndex = C.INDEX_UNSET; + inputBuffer.data = null; + } + + public void signalEndOfInputStream() { + mediaCodecAdapter.signalEndOfInputStream(); + } + + /** Returns the current output format, if available. */ + @Nullable + public Format getOutputFormat() { + // The format is updated when dequeueing a 'special' buffer index, so attempt to dequeue now. + maybeDequeueOutputBuffer(); + return outputFormat; + } + + /** Returns the current output {@link ByteBuffer}, if available. */ + @Nullable + public ByteBuffer getOutputBuffer() { + return maybeDequeueAndSetOutputBuffer() ? outputBuffer : null; + } + + /** Returns the {@link BufferInfo} associated with the current output buffer, if available. */ + @Nullable + public BufferInfo getOutputBufferInfo() { + return maybeDequeueOutputBuffer() ? outputBufferInfo : null; + } + + /** + * Releases the current output buffer. + * + *

          This should be called after the buffer has been processed. The next output buffer will not + * be available until the previous has been released. + */ + public void releaseOutputBuffer() { + releaseOutputBuffer(/* render= */ false); + } + + /** + * Releases the current output buffer. If the {@link MediaCodec} was configured with an output + * surface, setting {@code render} to {@code true} will first send the buffer to the output + * surface. The surface will release the buffer back to the codec once it is no longer + * used/displayed. + * + *

          This should be called after the buffer has been processed. The next output buffer will not + * be available until the previous has been released. + */ + public void releaseOutputBuffer(boolean render) { + outputBuffer = null; + mediaCodecAdapter.releaseOutputBuffer(outputBufferIndex, render); + outputBufferIndex = C.INDEX_UNSET; + } + + /** Returns whether the codec output stream has ended, and no more data can be dequeued. */ + public boolean isEnded() { + return outputStreamEnded && outputBufferIndex == C.INDEX_UNSET; + } + + /** Releases the underlying codec. */ + public void release() { + outputBuffer = null; + mediaCodecAdapter.release(); + } + + /** + * Tries obtaining an output buffer and sets {@link #outputBuffer} to the obtained output buffer. + * + * @return {@code true} if a buffer is successfully obtained, {@code false} otherwise. + */ + private boolean maybeDequeueAndSetOutputBuffer() { + if (!maybeDequeueOutputBuffer()) { + return false; + } + + outputBuffer = checkNotNull(mediaCodecAdapter.getOutputBuffer(outputBufferIndex)); + outputBuffer.position(outputBufferInfo.offset); + outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); + return true; + } + + /** + * Returns true if there is already an output buffer pending. Otherwise attempts to dequeue an + * output buffer and returns whether there is a new output buffer. + */ + private boolean maybeDequeueOutputBuffer() { + if (outputBufferIndex >= 0) { + return true; + } + if (outputStreamEnded) { + return false; + } + + outputBufferIndex = mediaCodecAdapter.dequeueOutputBufferIndex(outputBufferInfo); + if (outputBufferIndex < 0) { + if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + outputFormat = getFormat(mediaCodecAdapter.getOutputFormat()); + } + return false; + } + if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + outputStreamEnded = true; + if (outputBufferInfo.size == 0) { + releaseOutputBuffer(); + return false; + } + } + if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + // Encountered a CSD buffer, skip it. + releaseOutputBuffer(); + return false; + } + return true; + } + + private static Format getFormat(MediaFormat mediaFormat) { + ImmutableList.Builder csdBuffers = new ImmutableList.Builder<>(); + int csdIndex = 0; + while (true) { + @Nullable ByteBuffer csdByteBuffer = mediaFormat.getByteBuffer("csd-" + csdIndex); + if (csdByteBuffer == null) { + break; + } + byte[] csdBufferData = new byte[csdByteBuffer.remaining()]; + csdByteBuffer.get(csdBufferData); + csdBuffers.add(csdBufferData); + csdIndex++; + } + String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME); + Format.Builder formatBuilder = + new Format.Builder() + .setSampleMimeType(mediaFormat.getString(MediaFormat.KEY_MIME)) + .setInitializationData(csdBuffers.build()); + if (MimeTypes.isVideo(mimeType)) { + formatBuilder + .setWidth(mediaFormat.getInteger(MediaFormat.KEY_WIDTH)) + .setHeight(mediaFormat.getInteger(MediaFormat.KEY_HEIGHT)); + } else if (MimeTypes.isAudio(mimeType)) { + // TODO(internal b/178685617): Only set the PCM encoding for audio/raw, once we have a way to + // simulate more realistic codec input/output formats in tests. + formatBuilder + .setChannelCount(mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)) + .setSampleRate(mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)) + .setPcmEncoding(MEDIA_CODEC_PCM_ENCODING); + } + return formatBuilder.build(); + } +} diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/DefaultCodecFactory.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/DefaultCodecFactory.java new file mode 100644 index 0000000000..c6ace32a6b --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/DefaultCodecFactory.java @@ -0,0 +1,194 @@ +/* + * Copyright 2021 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.util.Assertions.checkArgument; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Util.SDK_INT; + +import android.annotation.SuppressLint; +import android.media.MediaCodec; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaFormat; +import android.view.Surface; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; +import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; +import com.google.android.exoplayer2.mediacodec.SynchronousMediaCodecAdapter; +import com.google.android.exoplayer2.util.MediaFormatUtil; +import java.io.IOException; + +/** A default {@link Codec.DecoderFactory} and {@link Codec.EncoderFactory}. */ +/* package */ final class DefaultCodecFactory + implements Codec.DecoderFactory, Codec.EncoderFactory { + + @Override + public Codec createForAudioDecoding(Format format) throws TransformationException { + MediaFormat mediaFormat = + MediaFormat.createAudioFormat( + checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); + MediaFormatUtil.maybeSetInteger( + mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); + MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); + + MediaCodecAdapter adapter; + try { + adapter = + new MediaCodecFactory() + .createAdapter( + MediaCodecAdapter.Configuration.createForAudioDecoding( + createPlaceholderMediaCodecInfo(), mediaFormat, format, /* crypto= */ null)); + } catch (Exception e) { + throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ true); + } + return new Codec(adapter); + } + + @Override + @SuppressLint("InlinedApi") + public Codec createForVideoDecoding(Format format, Surface surface) + throws TransformationException { + MediaFormat mediaFormat = + MediaFormat.createVideoFormat( + checkNotNull(format.sampleMimeType), format.width, format.height); + MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees); + MediaFormatUtil.maybeSetInteger( + mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); + MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); + if (SDK_INT >= 29) { + // On API levels over 29, Transformer decodes as many frames as possible in one render + // cycle. This key ensures no frame dropping when the decoder's output surface is full. + mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0); + } + + MediaCodecAdapter adapter; + try { + adapter = + new MediaCodecFactory() + .createAdapter( + MediaCodecAdapter.Configuration.createForVideoDecoding( + createPlaceholderMediaCodecInfo(), + mediaFormat, + format, + surface, + /* crypto= */ null)); + } catch (Exception e) { + throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ true); + } + return new Codec(adapter); + } + + @Override + public Codec createForAudioEncoding(Format format) throws TransformationException { + MediaFormat mediaFormat = + MediaFormat.createAudioFormat( + checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate); + + MediaCodecAdapter adapter; + try { + adapter = + new MediaCodecFactory() + .createAdapter( + MediaCodecAdapter.Configuration.createForAudioEncoding( + createPlaceholderMediaCodecInfo(), mediaFormat, format)); + } catch (Exception e) { + throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ false); + } + return new Codec(adapter); + } + + @Override + public Codec createForVideoEncoding(Format format) throws TransformationException { + checkArgument(format.width != Format.NO_VALUE); + checkArgument(format.height != Format.NO_VALUE); + // According to interface Javadoc, format.rotationDegrees should be 0. The video should always + // be in landscape orientation. + checkArgument(format.height < format.width); + checkArgument(format.rotationDegrees == 0); + + MediaFormat mediaFormat = + MediaFormat.createVideoFormat( + checkNotNull(format.sampleMimeType), format.width, format.height); + mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface); + mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30); + mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 413_000); + + MediaCodecAdapter adapter; + try { + adapter = + new MediaCodecFactory() + .createAdapter( + MediaCodecAdapter.Configuration.createForVideoEncoding( + createPlaceholderMediaCodecInfo(), mediaFormat, format)); + } catch (Exception e) { + throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ false); + } + return new Codec(adapter); + } + + private static final class MediaCodecFactory extends SynchronousMediaCodecAdapter.Factory { + @Override + protected MediaCodec createCodec(MediaCodecAdapter.Configuration configuration) + throws IOException { + String sampleMimeType = + checkNotNull(configuration.mediaFormat.getString(MediaFormat.KEY_MIME)); + boolean isDecoder = (configuration.flags & MediaCodec.CONFIGURE_FLAG_ENCODE) == 0; + return isDecoder + ? MediaCodec.createDecoderByType(checkNotNull(sampleMimeType)) + : MediaCodec.createEncoderByType(checkNotNull(sampleMimeType)); + } + } + + private static MediaCodecInfo createPlaceholderMediaCodecInfo() { + return MediaCodecInfo.newInstance( + /* name= */ "name-placeholder", + /* mimeType= */ "mime-type-placeholder", + /* codecMimeType= */ "mime-type-placeholder", + /* capabilities= */ null, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ false, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false); + } + + private static TransformationException createTransformationException( + Exception cause, Format format, boolean isVideo, boolean isDecoder) { + String componentName = (isVideo ? "Video" : "Audio") + (isDecoder ? "Decoder" : "Encoder"); + if (cause instanceof IOException) { + return TransformationException.createForCodec( + cause, + componentName, + format, + isDecoder + ? TransformationException.ERROR_CODE_DECODER_INIT_FAILED + : TransformationException.ERROR_CODE_ENCODER_INIT_FAILED); + } + if (cause instanceof IllegalArgumentException) { + return TransformationException.createForCodec( + cause, + componentName, + format, + isDecoder + ? TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED + : TransformationException.ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED); + } + return TransformationException.createForUnexpected(cause); + } +} diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MediaCodecAdapterWrapper.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MediaCodecAdapterWrapper.java deleted file mode 100644 index f0e11b698a..0000000000 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MediaCodecAdapterWrapper.java +++ /dev/null @@ -1,471 +0,0 @@ -/* - * Copyright 2021 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.util.Assertions.checkArgument; -import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; -import static com.google.android.exoplayer2.util.Util.SDK_INT; - -import android.annotation.SuppressLint; -import android.media.MediaCodec; -import android.media.MediaCodec.BufferInfo; -import android.media.MediaCodecInfo.CodecCapabilities; -import android.media.MediaFormat; -import android.view.Surface; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; -import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter.Configuration; -import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; -import com.google.android.exoplayer2.mediacodec.SynchronousMediaCodecAdapter; -import com.google.android.exoplayer2.util.MediaFormatUtil; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Map; -import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; - -/** - * A wrapper around {@link MediaCodecAdapter}. - * - *

          Provides a layer of abstraction for callers that need to interact with {@link MediaCodec} - * through {@link MediaCodecAdapter}. This is done by simplifying the calls needed to queue and - * dequeue buffers, removing the need to track buffer indices and codec events. - */ -/* package */ final class MediaCodecAdapterWrapper { - - // MediaCodec decoders always output 16 bit PCM, unless configured to output PCM float. - // https://developer.android.com/reference/android/media/MediaCodec#raw-audio-buffers. - private static final int MEDIA_CODEC_PCM_ENCODING = C.ENCODING_PCM_16BIT; - - private final BufferInfo outputBufferInfo; - private final MediaCodecAdapter codec; - - private @MonotonicNonNull Format outputFormat; - @Nullable private ByteBuffer outputBuffer; - - private int inputBufferIndex; - private int outputBufferIndex; - private boolean inputStreamEnded; - private boolean outputStreamEnded; - - private static class Factory extends SynchronousMediaCodecAdapter.Factory { - @Override - protected MediaCodec createCodec(Configuration configuration) throws IOException { - String sampleMimeType = - checkNotNull(configuration.mediaFormat.getString(MediaFormat.KEY_MIME)); - boolean isDecoder = (configuration.flags & MediaCodec.CONFIGURE_FLAG_ENCODE) == 0; - return isDecoder - ? MediaCodec.createDecoderByType(checkNotNull(sampleMimeType)) - : MediaCodec.createEncoderByType(checkNotNull(sampleMimeType)); - } - } - - private static MediaCodecInfo createPlaceholderMediaCodecInfo() { - return MediaCodecInfo.newInstance( - /* name= */ "name-placeholder", - /* mimeType= */ "mime-type-placeholder", - /* codecMimeType= */ "mime-type-placeholder", - /* capabilities= */ null, - /* hardwareAccelerated= */ false, - /* softwareOnly= */ false, - /* vendor= */ false, - /* forceDisableAdaptive= */ false, - /* forceSecure= */ false); - } - - /** - * Returns a {@link MediaCodecAdapterWrapper} for a configured and started {@link - * MediaCodecAdapter} audio decoder. - * - * @param format The {@link Format} (of the input data) used to determine the underlying {@link - * MediaCodec} and its configuration values. - * @return A configured and started decoder wrapper. - * @throws TransformationException If the underlying codec cannot be created. - */ - public static MediaCodecAdapterWrapper createForAudioDecoding(Format format) - throws TransformationException { - MediaFormat mediaFormat = - MediaFormat.createAudioFormat( - checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); - MediaFormatUtil.maybeSetInteger( - mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); - MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); - - MediaCodecAdapter adapter; - try { - adapter = - new Factory() - .createAdapter( - MediaCodecAdapter.Configuration.createForAudioDecoding( - createPlaceholderMediaCodecInfo(), mediaFormat, format, /* crypto= */ null)); - } catch (Exception e) { - throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ true); - } - return new MediaCodecAdapterWrapper(adapter); - } - - /** - * Returns a {@link MediaCodecAdapterWrapper} for a configured and started {@link - * MediaCodecAdapter} video decoder. - * - * @param format The {@link Format} (of the input data) used to determine the underlying {@link - * MediaCodec} and its configuration values. - * @param surface The {@link Surface} to which the decoder output is rendered. - * @return A configured and started decoder wrapper. - * @throws TransformationException If the underlying codec cannot be created. - */ - @SuppressLint("InlinedApi") - public static MediaCodecAdapterWrapper createForVideoDecoding(Format format, Surface surface) - throws TransformationException { - MediaFormat mediaFormat = - MediaFormat.createVideoFormat( - checkNotNull(format.sampleMimeType), format.width, format.height); - MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees); - MediaFormatUtil.maybeSetInteger( - mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); - MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); - if (SDK_INT >= 29) { - // On API levels over 29, Transformer decodes as many frames as possible in one render - // cycle. This key ensures no frame dropping when the decoder's output surface is full. - mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0); - } - - MediaCodecAdapter adapter; - try { - adapter = - new Factory() - .createAdapter( - MediaCodecAdapter.Configuration.createForVideoDecoding( - createPlaceholderMediaCodecInfo(), - mediaFormat, - format, - surface, - /* crypto= */ null)); - } catch (Exception e) { - throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ true); - } - return new MediaCodecAdapterWrapper(adapter); - } - - /** - * Returns a {@link MediaCodecAdapterWrapper} for a configured and started {@link - * MediaCodecAdapter} audio encoder. - * - * @param format The {@link Format} (of the output data) used to determine the underlying {@link - * MediaCodec} and its configuration values. - * @return A configured and started encoder wrapper. - * @throws TransformationException If the underlying codec cannot be created. - */ - public static MediaCodecAdapterWrapper createForAudioEncoding(Format format) - throws TransformationException { - MediaFormat mediaFormat = - MediaFormat.createAudioFormat( - checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); - mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate); - - MediaCodecAdapter adapter; - try { - adapter = - new Factory() - .createAdapter( - MediaCodecAdapter.Configuration.createForAudioEncoding( - createPlaceholderMediaCodecInfo(), mediaFormat, format)); - } catch (Exception e) { - throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ false); - } - return new MediaCodecAdapterWrapper(adapter); - } - - /** - * Returns a {@link MediaCodecAdapterWrapper} for a configured and started {@link - * MediaCodecAdapter} video encoder. - * - * @param format The {@link Format} (of the output data) used to determine the underlying {@link - * MediaCodec} and its configuration values. {@link Format#sampleMimeType}, {@link - * Format#width} and {@link Format#height} must be set to those of the desired output video - * format. {@link Format#rotationDegrees} should be 0. The video should always be in landscape - * orientation. - * @param additionalEncoderConfig A map of {@link MediaFormat}'s integer settings, where the keys - * are from {@code MediaFormat.KEY_*} constants. Its values will override those in {@code - * format}. - * @return A configured and started encoder wrapper. - * @throws TransformationException If the underlying codec cannot be created. - */ - public static MediaCodecAdapterWrapper createForVideoEncoding( - Format format, Map additionalEncoderConfig) throws TransformationException { - checkArgument(format.width != Format.NO_VALUE); - checkArgument(format.height != Format.NO_VALUE); - checkArgument(format.height < format.width); - checkArgument(format.rotationDegrees == 0); - - MediaFormat mediaFormat = - MediaFormat.createVideoFormat( - checkNotNull(format.sampleMimeType), format.width, format.height); - mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface); - mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30); - mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); - mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 413_000); - for (Map.Entry encoderSetting : additionalEncoderConfig.entrySet()) { - mediaFormat.setInteger(encoderSetting.getKey(), encoderSetting.getValue()); - } - - MediaCodecAdapter adapter; - try { - adapter = - new Factory() - .createAdapter( - MediaCodecAdapter.Configuration.createForVideoEncoding( - createPlaceholderMediaCodecInfo(), mediaFormat, format)); - } catch (Exception e) { - throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ false); - } - return new MediaCodecAdapterWrapper(adapter); - } - - private MediaCodecAdapterWrapper(MediaCodecAdapter codec) { - this.codec = codec; - outputBufferInfo = new BufferInfo(); - inputBufferIndex = C.INDEX_UNSET; - outputBufferIndex = C.INDEX_UNSET; - } - - /** Returns the input {@link Surface}, or null if the input is not a surface. */ - @Nullable - public Surface getInputSurface() { - return codec.getInputSurface(); - } - - /** - * Dequeues a writable input buffer, if available. - * - * @param inputBuffer The buffer where the dequeued buffer data is stored. - * @return Whether an input buffer is ready to be used. - */ - @EnsuresNonNullIf(expression = "#1.data", result = true) - public boolean maybeDequeueInputBuffer(DecoderInputBuffer inputBuffer) { - if (inputStreamEnded) { - return false; - } - if (inputBufferIndex < 0) { - inputBufferIndex = codec.dequeueInputBufferIndex(); - if (inputBufferIndex < 0) { - return false; - } - inputBuffer.data = codec.getInputBuffer(inputBufferIndex); - inputBuffer.clear(); - } - checkNotNull(inputBuffer.data); - return true; - } - - /** - * Queues an input buffer to the decoder. No buffers may be queued after an {@link - * DecoderInputBuffer#isEndOfStream() end of stream} buffer has been queued. - */ - public void queueInputBuffer(DecoderInputBuffer inputBuffer) { - checkState( - !inputStreamEnded, "Input buffer can not be queued after the input stream has ended."); - - int offset = 0; - int size = 0; - if (inputBuffer.data != null && inputBuffer.data.hasRemaining()) { - offset = inputBuffer.data.position(); - size = inputBuffer.data.remaining(); - } - int flags = 0; - if (inputBuffer.isEndOfStream()) { - inputStreamEnded = true; - flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM; - } - codec.queueInputBuffer(inputBufferIndex, offset, size, inputBuffer.timeUs, flags); - inputBufferIndex = C.INDEX_UNSET; - inputBuffer.data = null; - } - - public void signalEndOfInputStream() { - codec.signalEndOfInputStream(); - } - - /** Returns the current output format, if available. */ - @Nullable - public Format getOutputFormat() { - // The format is updated when dequeueing a 'special' buffer index, so attempt to dequeue now. - maybeDequeueOutputBuffer(); - return outputFormat; - } - - /** Returns the current output {@link ByteBuffer}, if available. */ - @Nullable - public ByteBuffer getOutputBuffer() { - return maybeDequeueAndSetOutputBuffer() ? outputBuffer : null; - } - - /** Returns the {@link BufferInfo} associated with the current output buffer, if available. */ - @Nullable - public BufferInfo getOutputBufferInfo() { - return maybeDequeueOutputBuffer() ? outputBufferInfo : null; - } - - /** - * Releases the current output buffer. - * - *

          This should be called after the buffer has been processed. The next output buffer will not - * be available until the previous has been released. - */ - public void releaseOutputBuffer() { - releaseOutputBuffer(/* render= */ false); - } - - /** - * Releases the current output buffer. If the {@link MediaCodec} was configured with an output - * surface, setting {@code render} to {@code true} will first send the buffer to the output - * surface. The surface will release the buffer back to the codec once it is no longer - * used/displayed. - * - *

          This should be called after the buffer has been processed. The next output buffer will not - * be available until the previous has been released. - */ - public void releaseOutputBuffer(boolean render) { - outputBuffer = null; - codec.releaseOutputBuffer(outputBufferIndex, render); - outputBufferIndex = C.INDEX_UNSET; - } - - /** Returns whether the codec output stream has ended, and no more data can be dequeued. */ - public boolean isEnded() { - return outputStreamEnded && outputBufferIndex == C.INDEX_UNSET; - } - - /** Releases the underlying codec. */ - public void release() { - outputBuffer = null; - codec.release(); - } - - /** - * Tries obtaining an output buffer and sets {@link #outputBuffer} to the obtained output buffer. - * - * @return {@code true} if a buffer is successfully obtained, {@code false} otherwise. - */ - private boolean maybeDequeueAndSetOutputBuffer() { - if (!maybeDequeueOutputBuffer()) { - return false; - } - - outputBuffer = checkNotNull(codec.getOutputBuffer(outputBufferIndex)); - outputBuffer.position(outputBufferInfo.offset); - outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); - return true; - } - - /** - * Returns true if there is already an output buffer pending. Otherwise attempts to dequeue an - * output buffer and returns whether there is a new output buffer. - */ - private boolean maybeDequeueOutputBuffer() { - if (outputBufferIndex >= 0) { - return true; - } - if (outputStreamEnded) { - return false; - } - - outputBufferIndex = codec.dequeueOutputBufferIndex(outputBufferInfo); - if (outputBufferIndex < 0) { - if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - outputFormat = getFormat(codec.getOutputFormat()); - } - return false; - } - if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { - outputStreamEnded = true; - if (outputBufferInfo.size == 0) { - releaseOutputBuffer(); - return false; - } - } - if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { - // Encountered a CSD buffer, skip it. - releaseOutputBuffer(); - return false; - } - return true; - } - - private static Format getFormat(MediaFormat mediaFormat) { - ImmutableList.Builder csdBuffers = new ImmutableList.Builder<>(); - int csdIndex = 0; - while (true) { - @Nullable ByteBuffer csdByteBuffer = mediaFormat.getByteBuffer("csd-" + csdIndex); - if (csdByteBuffer == null) { - break; - } - byte[] csdBufferData = new byte[csdByteBuffer.remaining()]; - csdByteBuffer.get(csdBufferData); - csdBuffers.add(csdBufferData); - csdIndex++; - } - String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME); - Format.Builder formatBuilder = - new Format.Builder() - .setSampleMimeType(mediaFormat.getString(MediaFormat.KEY_MIME)) - .setInitializationData(csdBuffers.build()); - if (MimeTypes.isVideo(mimeType)) { - formatBuilder - .setWidth(mediaFormat.getInteger(MediaFormat.KEY_WIDTH)) - .setHeight(mediaFormat.getInteger(MediaFormat.KEY_HEIGHT)); - } else if (MimeTypes.isAudio(mimeType)) { - // TODO(internal b/178685617): Only set the PCM encoding for audio/raw, once we have a way to - // simulate more realistic codec input/output formats in tests. - formatBuilder - .setChannelCount(mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)) - .setSampleRate(mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)) - .setPcmEncoding(MEDIA_CODEC_PCM_ENCODING); - } - return formatBuilder.build(); - } - - private static TransformationException createTransformationException( - Exception cause, Format format, boolean isVideo, boolean isDecoder) { - String componentName = (isVideo ? "Video" : "Audio") + (isDecoder ? "Decoder" : "Encoder"); - if (cause instanceof IOException) { - return TransformationException.createForCodec( - cause, - componentName, - format, - isDecoder - ? TransformationException.ERROR_CODE_DECODER_INIT_FAILED - : TransformationException.ERROR_CODE_ENCODER_INIT_FAILED); - } - if (cause instanceof IllegalArgumentException) { - return TransformationException.createForCodec( - cause, - componentName, - format, - isDecoder - ? TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED - : TransformationException.ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED); - } - return TransformationException.createForUnexpected(cause); - } -} diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java index b30299a359..b310db74e4 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java @@ -107,6 +107,7 @@ public final class Transformer { private DebugViewProvider debugViewProvider; private Looper looper; private Clock clock; + private Codec.EncoderFactory encoderFactory; /** @deprecated Use {@link #Builder(Context)} instead. */ @Deprecated @@ -118,6 +119,7 @@ public final class Transformer { listener = new Listener() {}; looper = Util.getCurrentOrMainLooper(); clock = Clock.DEFAULT; + encoderFactory = Codec.EncoderFactory.DEFAULT; debugViewProvider = DebugViewProvider.NONE; } @@ -135,6 +137,7 @@ public final class Transformer { listener = new Listener() {}; looper = Util.getCurrentOrMainLooper(); clock = Clock.DEFAULT; + encoderFactory = Codec.EncoderFactory.DEFAULT; debugViewProvider = DebugViewProvider.NONE; } @@ -153,6 +156,7 @@ public final class Transformer { this.videoMimeType = transformer.transformation.videoMimeType; this.listener = transformer.listener; this.looper = transformer.looper; + this.encoderFactory = transformer.encoderFactory; this.debugViewProvider = transformer.debugViewProvider; this.clock = transformer.clock; } @@ -360,6 +364,18 @@ public final class Transformer { return this; } + /** + * Sets the {@link Codec.EncoderFactory} that will be used by the transformer. The default value + * is {@link Codec.EncoderFactory#DEFAULT}. + * + * @param encoderFactory The {@link Codec.EncoderFactory} instance. + * @return This builder. + */ + public Builder setEncoderFactory(Codec.EncoderFactory encoderFactory) { + this.encoderFactory = encoderFactory; + return this; + } + /** * Sets a provider for views to show diagnostic information (if available) during * transformation. This is intended for debugging. The default value is {@link @@ -448,6 +464,8 @@ public final class Transformer { listener, looper, clock, + encoderFactory, + Codec.DecoderFactory.DEFAULT, debugViewProvider); } @@ -536,6 +554,8 @@ public final class Transformer { private final Transformation transformation; private final Looper looper; private final Clock clock; + private final Codec.EncoderFactory encoderFactory; + private final Codec.DecoderFactory decoderFactory; private final Transformer.DebugViewProvider debugViewProvider; private Transformer.Listener listener; @@ -551,6 +571,8 @@ public final class Transformer { Transformer.Listener listener, Looper looper, Clock clock, + Codec.EncoderFactory encoderFactory, + Codec.DecoderFactory decoderFactory, Transformer.DebugViewProvider debugViewProvider) { checkState( !transformation.removeAudio || !transformation.removeVideo, @@ -562,6 +584,8 @@ public final class Transformer { this.listener = listener; this.looper = looper; this.clock = clock; + this.encoderFactory = encoderFactory; + this.decoderFactory = decoderFactory; this.debugViewProvider = debugViewProvider; progressState = PROGRESS_STATE_NO_TRANSFORMATION; } @@ -662,7 +686,12 @@ public final class Transformer { new ExoPlayer.Builder( context, new TransformerRenderersFactory( - context, muxerWrapper, transformation, debugViewProvider)) + context, + muxerWrapper, + transformation, + encoderFactory, + decoderFactory, + debugViewProvider)) .setMediaSourceFactory(mediaSourceFactory) .setTrackSelector(trackSelector) .setLoadControl(loadControl) @@ -751,16 +780,22 @@ public final class Transformer { private final MuxerWrapper muxerWrapper; private final TransformerMediaClock mediaClock; private final Transformation transformation; + private final Codec.EncoderFactory encoderFactory; + private final Codec.DecoderFactory decoderFactory; private final Transformer.DebugViewProvider debugViewProvider; public TransformerRenderersFactory( Context context, MuxerWrapper muxerWrapper, Transformation transformation, + Codec.EncoderFactory encoderFactory, + Codec.DecoderFactory decoderFactory, Transformer.DebugViewProvider debugViewProvider) { this.context = context; this.muxerWrapper = muxerWrapper; this.transformation = transformation; + this.encoderFactory = encoderFactory; + this.decoderFactory = decoderFactory; this.debugViewProvider = debugViewProvider; mediaClock = new TransformerMediaClock(); } @@ -776,13 +811,21 @@ public final class Transformer { Renderer[] renderers = new Renderer[rendererCount]; int index = 0; if (!transformation.removeAudio) { - renderers[index] = new TransformerAudioRenderer(muxerWrapper, mediaClock, transformation); + renderers[index] = + new TransformerAudioRenderer( + muxerWrapper, mediaClock, transformation, encoderFactory, decoderFactory); index++; } if (!transformation.removeVideo) { renderers[index] = new TransformerVideoRenderer( - context, muxerWrapper, mediaClock, transformation, debugViewProvider); + context, + muxerWrapper, + mediaClock, + transformation, + encoderFactory, + decoderFactory, + debugViewProvider); index++; } return renderers; diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java index 846aa090ef..1e25726f83 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java @@ -32,11 +32,19 @@ import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; private static final String TAG = "TAudioRenderer"; + private final Codec.EncoderFactory encoderFactory; + private final Codec.DecoderFactory decoderFactory; private final DecoderInputBuffer decoderInputBuffer; public TransformerAudioRenderer( - MuxerWrapper muxerWrapper, TransformerMediaClock mediaClock, Transformation transformation) { + MuxerWrapper muxerWrapper, + TransformerMediaClock mediaClock, + Transformation transformation, + Codec.EncoderFactory encoderFactory, + Codec.DecoderFactory decoderFactory) { super(C.TRACK_TYPE_AUDIO, muxerWrapper, mediaClock, transformation); + this.encoderFactory = encoderFactory; + this.decoderFactory = decoderFactory; decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); } @@ -60,7 +68,8 @@ import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; } Format inputFormat = checkNotNull(formatHolder.format); if (shouldTranscode(inputFormat)) { - samplePipeline = new AudioSamplePipeline(inputFormat, transformation); + samplePipeline = + new AudioSamplePipeline(inputFormat, transformation, encoderFactory, decoderFactory); } else { samplePipeline = new PassthroughSamplePipeline(inputFormat); } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java index dccd1d2ca3..ec062bfeb9 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java @@ -34,6 +34,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private static final String TAG = "TVideoRenderer"; private final Context context; + private final Codec.EncoderFactory encoderFactory; + private final Codec.DecoderFactory decoderFactory; private final Transformer.DebugViewProvider debugViewProvider; private final DecoderInputBuffer decoderInputBuffer; @@ -44,9 +46,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; MuxerWrapper muxerWrapper, TransformerMediaClock mediaClock, Transformation transformation, + Codec.EncoderFactory encoderFactory, + Codec.DecoderFactory decoderFactory, Transformer.DebugViewProvider debugViewProvider) { super(C.TRACK_TYPE_VIDEO, muxerWrapper, mediaClock, transformation); this.context = context; + this.encoderFactory = encoderFactory; + this.decoderFactory = decoderFactory; this.debugViewProvider = debugViewProvider; decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); @@ -72,7 +78,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; Format inputFormat = checkNotNull(formatHolder.format); if (shouldTranscode(inputFormat)) { samplePipeline = - new VideoSamplePipeline(context, inputFormat, transformation, debugViewProvider); + new VideoSamplePipeline( + context, + inputFormat, + transformation, + encoderFactory, + decoderFactory, + debugViewProvider); } else { samplePipeline = new PassthroughSamplePipeline(inputFormat); } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java index eab7aa803e..d23d58b4ca 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java @@ -27,7 +27,6 @@ import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.common.collect.ImmutableMap; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -39,9 +38,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final int outputRotationDegrees; private final DecoderInputBuffer decoderInputBuffer; - private final MediaCodecAdapterWrapper decoder; + private final Codec decoder; - private final MediaCodecAdapterWrapper encoder; + private final Codec encoder; private final DecoderInputBuffer encoderOutputBuffer; private @MonotonicNonNull FrameEditor frameEditor; @@ -52,6 +51,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Context context, Format inputFormat, Transformation transformation, + Codec.EncoderFactory encoderFactory, + Codec.DecoderFactory decoderFactory, Transformer.DebugViewProvider debugViewProvider) throws TransformationException { decoderInputBuffer = @@ -85,7 +86,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; transformation.transformationMatrix.postRotate(outputRotationDegrees); encoder = - MediaCodecAdapterWrapper.createForVideoEncoding( + encoderFactory.createForVideoEncoding( new Format.Builder() .setWidth(outputWidth) .setHeight(outputHeight) @@ -94,8 +95,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; transformation.videoMimeType != null ? transformation.videoMimeType : inputFormat.sampleMimeType) - .build(), - ImmutableMap.of()); + .build()); if (inputFormat.height != outputHeight || inputFormat.width != outputWidth || !transformation.transformationMatrix.isIdentity()) { @@ -109,7 +109,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; debugViewProvider); } decoder = - MediaCodecAdapterWrapper.createForVideoDecoding( + decoderFactory.createForVideoDecoding( inputFormat, frameEditor == null ? checkNotNull(encoder.getInputSurface()) From bb1357b6782cd08f7b9bb4577fcd947bc074ff7d Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 21 Dec 2021 16:07:18 +0000 Subject: [PATCH 38/56] Delete deprecated methods from `MediaSourceFactory` Some have been deprecated since 2.13.0 ([commit](https://github.com/google/ExoPlayer/commit/5b9fa7d7d9d68dec060489cbb307b1be28a7575a)): * `setDrmSessionManager(DrmSessionManager)` * `setDrmHttpDataSourceFactory(HttpDataSource.Factory)` * `setDrmUserAgent(String)` And the rest have been deprecated since 2.12.0 ([commit](https://github.com/google/ExoPlayer/commit/d1bbd3507a818e14be965c300938f9d51f8b7836)): * `setStreamKeys(List)` * `createMediaSource(Uri)` PiperOrigin-RevId: 417622794 --- RELEASENOTES.md | 9 ++ .../source/DefaultMediaSourceFactory.java | 87 --------------- .../exoplayer2/source/MediaSourceFactory.java | 104 ------------------ .../source/ProgressiveMediaSource.java | 52 +-------- .../source/dash/DashMediaSource.java | 92 +--------------- .../source/dash/DashMediaSourceTest.java | 41 ------- .../exoplayer2/source/hls/HlsMediaSource.java | 84 +------------- .../source/hls/HlsMediaSourceTest.java | 41 ------- .../source/rtsp/RtspMediaSource.java | 39 ------- .../source/smoothstreaming/SsMediaSource.java | 87 ++------------- .../smoothstreaming/SsMediaSourceTest.java | 43 -------- .../testutil/FakeMediaSourceFactory.java | 21 ---- 12 files changed, 34 insertions(+), 666 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fa7fea5868..5d7ac43b6a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -81,6 +81,15 @@ * RTSP: * Provide a client API to override the `SocketFactory` used for any server connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)). +* Remove deprecated symbols: + * Remove `MediaSourceFactory#setDrmSessionManager`, + `MediaSourceFactory#setDrmHttpDataSourceFactory`, and + `MediaSourceFactory#setDrmUserAgent`. Use + `MediaSourceFactory#setDrmSessionManagerProvider` instead. + * Remove `MediaSourceFactory#setStreamKeys`. Use + `MediaItem.Builder#setStreamKeys` instead. + * Remove `MediaSourceFactory#createMediaSource(Uri)`. Use + `MediaSourceFactory#createMediaSource(MediaItem)` instead. ### 2.16.1 (2021-11-18) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java index 38bcb12391..222d67c1ca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java @@ -24,7 +24,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManagerProvider; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; @@ -34,7 +33,6 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.text.SubtitleDecoderFactory; @@ -43,7 +41,6 @@ import com.google.android.exoplayer2.ui.AdViewProvider; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DefaultDataSource; -import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -280,29 +277,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { return this; } - @Deprecated - @Override - public DefaultMediaSourceFactory setDrmHttpDataSourceFactory( - @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { - delegateFactoryLoader.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory); - return this; - } - - @Deprecated - @Override - public DefaultMediaSourceFactory setDrmUserAgent(@Nullable String userAgent) { - delegateFactoryLoader.setDrmUserAgent(userAgent); - return this; - } - - @Deprecated - @Override - public DefaultMediaSourceFactory setDrmSessionManager( - @Nullable DrmSessionManager drmSessionManager) { - delegateFactoryLoader.setDrmSessionManager(drmSessionManager); - return this; - } - @Override public DefaultMediaSourceFactory setDrmSessionManagerProvider( @Nullable DrmSessionManagerProvider drmSessionManagerProvider) { @@ -318,18 +292,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { return this; } - /** - * @deprecated Use {@link MediaItem.Builder#setStreamKeys(List)} and {@link - * #createMediaSource(MediaItem)} instead. - */ - @SuppressWarnings("deprecation") // Calling through to the same deprecated method. - @Deprecated - @Override - public DefaultMediaSourceFactory setStreamKeys(@Nullable List streamKeys) { - delegateFactoryLoader.setStreamKeys(streamKeys); - return this; - } - @Override public int[] getSupportedTypes() { return delegateFactoryLoader.getSupportedTypes(); @@ -478,12 +440,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { private final Set supportedTypes; private final Map mediaSourceFactories; - @Nullable private HttpDataSource.Factory drmHttpDataSourceFactory; - @Nullable private String userAgent; - @Nullable private DrmSessionManager drmSessionManager; @Nullable private DrmSessionManagerProvider drmSessionManagerProvider; @Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy; - @Nullable private List streamKeys; public DelegateFactoryLoader( DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) { @@ -514,53 +472,16 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { } mediaSourceFactory = mediaSourceFactorySupplier.get(); - if (drmHttpDataSourceFactory != null) { - mediaSourceFactory.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory); - } - if (userAgent != null) { - mediaSourceFactory.setDrmUserAgent(userAgent); - } - if (drmSessionManager != null) { - mediaSourceFactory.setDrmSessionManager(drmSessionManager); - } if (drmSessionManagerProvider != null) { mediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider); } if (loadErrorHandlingPolicy != null) { mediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy); } - if (streamKeys != null) { - mediaSourceFactory.setStreamKeys(streamKeys); - } mediaSourceFactories.put(contentType, mediaSourceFactory); return mediaSourceFactory; } - @SuppressWarnings("deprecation") // Forwarding to deprecated method. - public void setDrmHttpDataSourceFactory( - @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { - this.drmHttpDataSourceFactory = drmHttpDataSourceFactory; - for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) { - mediaSourceFactory.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory); - } - } - - @SuppressWarnings("deprecation") // Forwarding to deprecated method. - public void setDrmUserAgent(@Nullable String userAgent) { - this.userAgent = userAgent; - for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) { - mediaSourceFactory.setDrmUserAgent(userAgent); - } - } - - @SuppressWarnings("deprecation") // Forwarding to deprecated method. - public void setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { - this.drmSessionManager = drmSessionManager; - for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) { - mediaSourceFactory.setDrmSessionManager(drmSessionManager); - } - } - public void setDrmSessionManagerProvider( @Nullable DrmSessionManagerProvider drmSessionManagerProvider) { this.drmSessionManagerProvider = drmSessionManagerProvider; @@ -577,14 +498,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { } } - @SuppressWarnings("deprecation") // Forwarding to deprecated method. - public void setStreamKeys(@Nullable List streamKeys) { - this.streamKeys = streamKeys; - for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) { - mediaSourceFactory.setStreamKeys(streamKeys); - } - } - private void ensureAllSuppliersAreLoaded() { maybeLoadSupplier(C.TYPE_DASH); maybeLoadSupplier(C.TYPE_SS); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java index 9a4896a95f..54d9eb2658 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java @@ -15,21 +15,14 @@ */ package com.google.android.exoplayer2.source; -import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManagerProvider; -import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; -import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; -import java.util.List; /** Factory for creating {@link MediaSource MediaSources} from {@link MediaItem MediaItems}. */ public interface MediaSourceFactory { @@ -46,26 +39,6 @@ public interface MediaSourceFactory { return this; } - @Deprecated - @Override - public MediaSourceFactory setDrmSessionManager( - @Nullable DrmSessionManager drmSessionManager) { - return this; - } - - @Deprecated - @Override - public MediaSourceFactory setDrmHttpDataSourceFactory( - @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { - return this; - } - - @Deprecated - @Override - public MediaSourceFactory setDrmUserAgent(@Nullable String userAgent) { - return this; - } - @Override public MediaSourceFactory setLoadErrorHandlingPolicy( @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { @@ -84,88 +57,17 @@ public interface MediaSourceFactory { } }; - /** @deprecated Use {@link MediaItem.LocalConfiguration#streamKeys} instead. */ - @Deprecated - default MediaSourceFactory setStreamKeys(@Nullable List streamKeys) { - return this; - } - /** * Sets the {@link DrmSessionManagerProvider} used to obtain a {@link DrmSessionManager} for a * {@link MediaItem}. * *

          If not set, {@link DefaultDrmSessionManagerProvider} is used. * - *

          If set, calls to the following (deprecated) methods are ignored: - * - *

            - *
          • {@link #setDrmUserAgent(String)} - *
          • {@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory)} - *
          - * * @return This factory, for convenience. */ MediaSourceFactory setDrmSessionManagerProvider( @Nullable DrmSessionManagerProvider drmSessionManagerProvider); - /** - * Sets the {@link DrmSessionManager} to use for all media items regardless of their {@link - * MediaItem.DrmConfiguration}. - * - *

          Calling this with a non-null {@code drmSessionManager} is equivalent to calling {@code - * setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)}. - * - * @param drmSessionManager The {@link DrmSessionManager}, or {@code null} to use the {@link - * DefaultDrmSessionManager}. - * @return This factory, for convenience. - * @deprecated Use {@link #setDrmSessionManagerProvider(DrmSessionManagerProvider)} and pass an - * implementation that always returns the same instance. - */ - @Deprecated - MediaSourceFactory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager); - - /** - * Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback - * HttpMediaDrmCallbacks} to execute key and provisioning requests over HTTP. - * - *

          Calls to this method are ignored if either a {@link - * #setDrmSessionManagerProvider(DrmSessionManagerProvider) DrmSessionManager provider} or {@link - * #setDrmSessionManager(DrmSessionManager) concrete DrmSessionManager} are provided. - * - * @param drmHttpDataSourceFactory The HTTP data source factory, or {@code null} to use {@link - * DefaultHttpDataSource.Factory}. - * @return This factory, for convenience. - * @deprecated Use {@link #setDrmSessionManagerProvider(DrmSessionManagerProvider)} and pass an - * implementation that configures the returned {@link DrmSessionManager} with the desired - * {@link HttpDataSource.Factory}. - */ - @Deprecated - MediaSourceFactory setDrmHttpDataSourceFactory( - @Nullable HttpDataSource.Factory drmHttpDataSourceFactory); - - /** - * Sets the optional user agent to be used for DRM requests. - * - *

          Calls to this method are ignored if any of the following are provided: - * - *

            - *
          • A {@link #setDrmSessionManagerProvider(DrmSessionManagerProvider) DrmSessionManager - * provider}. - *
          • A {@link #setDrmSessionManager(DrmSessionManager) concrete DrmSessionManager}. - *
          • A {@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory) DRM - * HttpDataSource.Factory}. - *
          - * - * @param userAgent The user agent to be used for DRM requests, or {@code null} to use the - * default. - * @return This factory, for convenience. - * @deprecated Use {@link #setDrmSessionManagerProvider(DrmSessionManagerProvider)} and pass an - * implementation that configures the returned {@link DrmSessionManager} with the desired - * {@code userAgent}. - */ - @Deprecated - MediaSourceFactory setDrmUserAgent(@Nullable String userAgent); - /** * Sets an optional {@link LoadErrorHandlingPolicy}. * @@ -190,10 +92,4 @@ public interface MediaSourceFactory { * @return The new {@link MediaSource media source}. */ MediaSource createMediaSource(MediaItem mediaItem); - - /** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */ - @Deprecated - default MediaSource createMediaSource(Uri uri) { - return createMediaSource(MediaItem.fromUri(uri)); - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index 1afcf5732d..53b2c90108 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; -import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; @@ -56,7 +55,6 @@ public final class ProgressiveMediaSource extends BaseMediaSource private final DataSource.Factory dataSourceFactory; private ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory; - private boolean usingCustomDrmSessionManagerProvider; private DrmSessionManagerProvider drmSessionManagerProvider; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private int continueLoadingCheckIntervalBytes; @@ -166,55 +164,13 @@ public final class ProgressiveMediaSource extends BaseMediaSource @Override public Factory setDrmSessionManagerProvider( @Nullable DrmSessionManagerProvider drmSessionManagerProvider) { - if (drmSessionManagerProvider != null) { - this.drmSessionManagerProvider = drmSessionManagerProvider; - this.usingCustomDrmSessionManagerProvider = true; - } else { - this.drmSessionManagerProvider = new DefaultDrmSessionManagerProvider(); - this.usingCustomDrmSessionManagerProvider = false; - } + this.drmSessionManagerProvider = + drmSessionManagerProvider != null + ? drmSessionManagerProvider + : new DefaultDrmSessionManagerProvider(); return this; } - @Deprecated - @Override - public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { - if (drmSessionManager == null) { - setDrmSessionManagerProvider(null); - } else { - setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager); - } - return this; - } - - @Deprecated - @Override - public Factory setDrmHttpDataSourceFactory( - @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { - if (!usingCustomDrmSessionManagerProvider) { - ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider) - .setDrmHttpDataSourceFactory(drmHttpDataSourceFactory); - } - return this; - } - - @Deprecated - @Override - public Factory setDrmUserAgent(@Nullable String userAgent) { - if (!usingCustomDrmSessionManagerProvider) { - ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider).setDrmUserAgent(userAgent); - } - return this; - } - - /** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */ - @SuppressWarnings("deprecation") - @Deprecated - @Override - public ProgressiveMediaSource createMediaSource(Uri uri) { - return createMediaSource(new MediaItem.Builder().setUri(uri).build()); - } - /** * Returns a new {@link ProgressiveMediaSource} using the current parameters. * diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index d83e194e60..7b6df6cbf2 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -62,7 +62,6 @@ import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; -import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; import com.google.android.exoplayer2.upstream.Loader; @@ -84,7 +83,6 @@ import java.io.InputStreamReader; import java.math.RoundingMode; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.TimeZone; @@ -104,14 +102,12 @@ public final class DashMediaSource extends BaseMediaSource { private final DashChunkSource.Factory chunkSourceFactory; @Nullable private final DataSource.Factory manifestDataSourceFactory; - private boolean usingCustomDrmSessionManagerProvider; private DrmSessionManagerProvider drmSessionManagerProvider; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long targetLiveOffsetOverrideMs; private long fallbackTargetLiveOffsetMs; @Nullable private ParsingLoadable.Parser manifestParser; - private List streamKeys; @Nullable private Object tag; /** @@ -143,7 +139,6 @@ public final class DashMediaSource extends BaseMediaSource { targetLiveOffsetOverrideMs = C.TIME_UNSET; fallbackTargetLiveOffsetMs = DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); - streamKeys = Collections.emptyList(); } /** @@ -156,59 +151,13 @@ public final class DashMediaSource extends BaseMediaSource { return this; } - /** - * @deprecated Use {@link MediaItem.Builder#setStreamKeys(List)} and {@link - * #createMediaSource(MediaItem)} instead. - */ - @SuppressWarnings("deprecation") - @Deprecated - @Override - public Factory setStreamKeys(@Nullable List streamKeys) { - this.streamKeys = streamKeys != null ? streamKeys : Collections.emptyList(); - return this; - } - @Override public Factory setDrmSessionManagerProvider( @Nullable DrmSessionManagerProvider drmSessionManagerProvider) { - if (drmSessionManagerProvider != null) { - this.drmSessionManagerProvider = drmSessionManagerProvider; - this.usingCustomDrmSessionManagerProvider = true; - } else { - this.drmSessionManagerProvider = new DefaultDrmSessionManagerProvider(); - this.usingCustomDrmSessionManagerProvider = false; - } - return this; - } - - @Deprecated - @Override - public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { - if (drmSessionManager == null) { - setDrmSessionManagerProvider(null); - } else { - setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager); - } - return this; - } - - @Deprecated - @Override - public Factory setDrmHttpDataSourceFactory( - @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { - if (!usingCustomDrmSessionManagerProvider) { - ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider) - .setDrmHttpDataSourceFactory(drmHttpDataSourceFactory); - } - return this; - } - - @Deprecated - @Override - public Factory setDrmUserAgent(@Nullable String userAgent) { - if (!usingCustomDrmSessionManagerProvider) { - ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider).setDrmUserAgent(userAgent); - } + this.drmSessionManagerProvider = + drmSessionManagerProvider != null + ? drmSessionManagerProvider + : new DefaultDrmSessionManagerProvider(); return this; } @@ -303,7 +252,6 @@ public final class DashMediaSource extends BaseMediaSource { .setUri(Uri.EMPTY) .setMediaId(DEFAULT_MEDIA_ID) .setMimeType(MimeTypes.APPLICATION_MPD) - .setStreamKeys(streamKeys) .setTag(tag) .build()); } @@ -335,14 +283,7 @@ public final class DashMediaSource extends BaseMediaSource { .setTargetOffsetMs(targetLiveOffsetOverrideMs) .build()); } - if (mediaItem.localConfiguration == null - || mediaItem.localConfiguration.streamKeys.isEmpty()) { - mediaItemBuilder.setStreamKeys(streamKeys); - } mediaItem = mediaItemBuilder.build(); - if (!checkNotNull(mediaItem.localConfiguration).streamKeys.isEmpty()) { - manifest = manifest.copy(streamKeys); - } return new DashMediaSource( mediaItem, manifest, @@ -355,19 +296,6 @@ public final class DashMediaSource extends BaseMediaSource { fallbackTargetLiveOffsetMs); } - /** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */ - @SuppressWarnings("deprecation") - @Deprecated - @Override - public DashMediaSource createMediaSource(Uri uri) { - return createMediaSource( - new MediaItem.Builder() - .setUri(uri) - .setMimeType(MimeTypes.APPLICATION_MPD) - .setTag(tag) - .build()); - } - /** * Returns a new {@link DashMediaSource} using the current parameters. * @@ -382,28 +310,20 @@ public final class DashMediaSource extends BaseMediaSource { if (manifestParser == null) { manifestParser = new DashManifestParser(); } - List streamKeys = - mediaItem.localConfiguration.streamKeys.isEmpty() - ? this.streamKeys - : mediaItem.localConfiguration.streamKeys; + List streamKeys = mediaItem.localConfiguration.streamKeys; if (!streamKeys.isEmpty()) { manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys); } boolean needsTag = mediaItem.localConfiguration.tag == null && tag != null; - boolean needsStreamKeys = - mediaItem.localConfiguration.streamKeys.isEmpty() && !streamKeys.isEmpty(); boolean needsTargetLiveOffset = mediaItem.liveConfiguration.targetOffsetMs == C.TIME_UNSET && targetLiveOffsetOverrideMs != C.TIME_UNSET; - if (needsTag || needsStreamKeys || needsTargetLiveOffset) { + if (needsTag || needsTargetLiveOffset) { MediaItem.Builder builder = mediaItem.buildUpon(); if (needsTag) { builder.setTag(tag); } - if (needsStreamKeys) { - builder.setStreamKeys(streamKeys); - } if (needsTargetLiveOffset) { builder.setLiveConfiguration( mediaItem diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java index 74ef8ce851..f66ec8a7eb 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java @@ -28,7 +28,6 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.analytics.PlayerId; -import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.testutil.TestUtil; @@ -37,7 +36,6 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Util; -import com.google.common.collect.ImmutableList; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.concurrent.CountDownLatch; @@ -135,45 +133,6 @@ public final class DashMediaSourceTest { assertThat(dashMediaItem.localConfiguration.tag).isEqualTo(mediaItemTag); } - // Tests backwards compatibility - @SuppressWarnings("deprecation") - @Test - public void factorySetStreamKeys_emptyMediaItemStreamKeys_setsMediaItemStreamKeys() { - MediaItem mediaItem = MediaItem.fromUri("http://www.google.com"); - StreamKey streamKey = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 1); - DashMediaSource.Factory factory = - new DashMediaSource.Factory(new FileDataSource.Factory()) - .setStreamKeys(ImmutableList.of(streamKey)); - - MediaItem dashMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(dashMediaItem.localConfiguration).isNotNull(); - assertThat(dashMediaItem.localConfiguration.uri).isEqualTo(mediaItem.localConfiguration.uri); - assertThat(dashMediaItem.localConfiguration.streamKeys).containsExactly(streamKey); - } - - // Tests backwards compatibility - @SuppressWarnings("deprecation") - @Test - public void factorySetStreamKeys_withMediaItemStreamKeys_doesNotOverrideMediaItemStreamKeys() { - StreamKey mediaItemStreamKey = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 1); - MediaItem mediaItem = - new MediaItem.Builder() - .setUri("http://www.google.com") - .setStreamKeys(ImmutableList.of(mediaItemStreamKey)) - .build(); - DashMediaSource.Factory factory = - new DashMediaSource.Factory(new FileDataSource.Factory()) - .setStreamKeys( - ImmutableList.of(new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 0))); - - MediaItem dashMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(dashMediaItem.localConfiguration).isNotNull(); - assertThat(dashMediaItem.localConfiguration.uri).isEqualTo(mediaItem.localConfiguration.uri); - assertThat(dashMediaItem.localConfiguration.streamKeys).containsExactly(mediaItemStreamKey); - } - @Test public void replaceManifestUri_doesNotChangeMediaItem() { DashMediaSource.Factory factory = new DashMediaSource.Factory(new FileDataSource.Factory()); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 152f6d9abe..43e9645dc5 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source.hls; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static java.lang.annotation.RetentionPolicy.SOURCE; -import android.net.Uri; import android.os.Looper; import android.os.SystemClock; import androidx.annotation.IntDef; @@ -51,17 +50,14 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; -import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import java.util.Collections; import java.util.List; /** An HLS {@link MediaSource}. */ @@ -104,13 +100,11 @@ public final class HlsMediaSource extends BaseMediaSource private HlsPlaylistParserFactory playlistParserFactory; private HlsPlaylistTracker.Factory playlistTrackerFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private boolean usingCustomDrmSessionManagerProvider; private DrmSessionManagerProvider drmSessionManagerProvider; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean allowChunklessPreparation; private @MetadataType int metadataType; private boolean useSessionKeys; - private List streamKeys; @Nullable private Object tag; private long elapsedRealTimeOffsetMs; @@ -140,7 +134,6 @@ public final class HlsMediaSource extends BaseMediaSource loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); metadataType = METADATA_TYPE_ID3; - streamKeys = Collections.emptyList(); elapsedRealTimeOffsetMs = C.TIME_UNSET; allowChunklessPreparation = true; } @@ -290,56 +283,10 @@ public final class HlsMediaSource extends BaseMediaSource @Override public Factory setDrmSessionManagerProvider( @Nullable DrmSessionManagerProvider drmSessionManagerProvider) { - if (drmSessionManagerProvider != null) { - this.drmSessionManagerProvider = drmSessionManagerProvider; - this.usingCustomDrmSessionManagerProvider = true; - } else { - this.drmSessionManagerProvider = new DefaultDrmSessionManagerProvider(); - this.usingCustomDrmSessionManagerProvider = false; - } - return this; - } - - @Deprecated - @Override - public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { - if (drmSessionManager == null) { - setDrmSessionManagerProvider(null); - } else { - setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager); - } - return this; - } - - @Deprecated - @Override - public Factory setDrmHttpDataSourceFactory( - @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { - if (!usingCustomDrmSessionManagerProvider) { - ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider) - .setDrmHttpDataSourceFactory(drmHttpDataSourceFactory); - } - return this; - } - - @Deprecated - @Override - public Factory setDrmUserAgent(@Nullable String userAgent) { - if (!usingCustomDrmSessionManagerProvider) { - ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider).setDrmUserAgent(userAgent); - } - return this; - } - - /** - * @deprecated Use {@link MediaItem.Builder#setStreamKeys(List)} and {@link - * #createMediaSource(MediaItem)} instead. - */ - @SuppressWarnings("deprecation") - @Deprecated - @Override - public Factory setStreamKeys(@Nullable List streamKeys) { - this.streamKeys = streamKeys != null ? streamKeys : Collections.emptyList(); + this.drmSessionManagerProvider = + drmSessionManagerProvider != null + ? drmSessionManagerProvider + : new DefaultDrmSessionManagerProvider(); return this; } @@ -357,15 +304,6 @@ public final class HlsMediaSource extends BaseMediaSource return this; } - /** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */ - @SuppressWarnings("deprecation") - @Deprecated - @Override - public HlsMediaSource createMediaSource(Uri uri) { - return createMediaSource( - new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_M3U8).build()); - } - /** * Returns a new {@link HlsMediaSource} using the current parameters. * @@ -377,24 +315,14 @@ public final class HlsMediaSource extends BaseMediaSource public HlsMediaSource createMediaSource(MediaItem mediaItem) { checkNotNull(mediaItem.localConfiguration); HlsPlaylistParserFactory playlistParserFactory = this.playlistParserFactory; - List streamKeys = - mediaItem.localConfiguration.streamKeys.isEmpty() - ? this.streamKeys - : mediaItem.localConfiguration.streamKeys; + List streamKeys = mediaItem.localConfiguration.streamKeys; if (!streamKeys.isEmpty()) { playlistParserFactory = new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys); } - boolean needsTag = mediaItem.localConfiguration.tag == null && tag != null; - boolean needsStreamKeys = - mediaItem.localConfiguration.streamKeys.isEmpty() && !streamKeys.isEmpty(); - if (needsTag && needsStreamKeys) { - mediaItem = mediaItem.buildUpon().setTag(tag).setStreamKeys(streamKeys).build(); - } else if (needsTag) { + if (mediaItem.localConfiguration.tag == null && tag != null) { mediaItem = mediaItem.buildUpon().setTag(tag).build(); - } else if (needsStreamKeys) { - mediaItem = mediaItem.buildUpon().setStreamKeys(streamKeys).build(); } return new HlsMediaSource( mediaItem, diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java index 6e1e298184..81c9709c09 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java @@ -26,7 +26,6 @@ import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.PlayerId; -import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; @@ -37,7 +36,6 @@ import com.google.android.exoplayer2.util.Util; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; @@ -82,45 +80,6 @@ public class HlsMediaSourceTest { assertThat(hlsMediaItem.localConfiguration.tag).isEqualTo(mediaItemTag); } - // Tests backwards compatibility - @SuppressWarnings("deprecation") - @Test - public void factorySetStreamKeys_emptyMediaItemStreamKeys_setsMediaItemStreamKeys() { - MediaItem mediaItem = MediaItem.fromUri("http://www.google.com"); - StreamKey streamKey = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 1); - HlsMediaSource.Factory factory = - new HlsMediaSource.Factory(mock(DataSource.Factory.class)) - .setStreamKeys(Collections.singletonList(streamKey)); - - MediaItem hlsMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(hlsMediaItem.localConfiguration).isNotNull(); - assertThat(hlsMediaItem.localConfiguration.uri).isEqualTo(mediaItem.localConfiguration.uri); - assertThat(hlsMediaItem.localConfiguration.streamKeys).containsExactly(streamKey); - } - - // Tests backwards compatibility - @SuppressWarnings("deprecation") - @Test - public void factorySetStreamKeys_withMediaItemStreamKeys_doesNotOverrideMediaItemStreamKeys() { - StreamKey mediaItemStreamKey = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 1); - MediaItem mediaItem = - new MediaItem.Builder() - .setUri("http://www.google.com") - .setStreamKeys(Collections.singletonList(mediaItemStreamKey)) - .build(); - HlsMediaSource.Factory factory = - new HlsMediaSource.Factory(mock(DataSource.Factory.class)) - .setStreamKeys( - Collections.singletonList(new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 0))); - - MediaItem hlsMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(hlsMediaItem.localConfiguration).isNotNull(); - assertThat(hlsMediaItem.localConfiguration.uri).isEqualTo(mediaItem.localConfiguration.uri); - assertThat(hlsMediaItem.localConfiguration.streamKeys).containsExactly(mediaItemStreamKey); - } - @Test public void loadLivePlaylist_noTargetLiveOffsetDefined_fallbackToThreeTargetDuration() throws TimeoutException, ParserException { diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java index 7e9dbab47c..ddfb70651c 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManagerProvider; import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.ForwardingTimeline; @@ -36,7 +35,6 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Util; @@ -60,9 +58,6 @@ public final class RtspMediaSource extends BaseMediaSource { * *
            *
          • {@link #setDrmSessionManagerProvider(DrmSessionManagerProvider)} - *
          • {@link #setDrmSessionManager(DrmSessionManager)} - *
          • {@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory)} - *
          • {@link #setDrmUserAgent(String)} *
          • {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} *
          */ @@ -155,40 +150,6 @@ public final class RtspMediaSource extends BaseMediaSource { return this; } - /** - * Does nothing. {@link RtspMediaSource} does not support DRM. - * - * @deprecated {@link RtspMediaSource} does not support DRM. - */ - @Deprecated - @Override - public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { - return this; - } - - /** - * Does nothing. {@link RtspMediaSource} does not support DRM. - * - * @deprecated {@link RtspMediaSource} does not support DRM. - */ - @Deprecated - @Override - public Factory setDrmHttpDataSourceFactory( - @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { - return this; - } - - /** - * Does nothing. {@link RtspMediaSource} does not support DRM. - * - * @deprecated {@link RtspMediaSource} does not support DRM. - */ - @Deprecated - @Override - public Factory setDrmUserAgent(@Nullable String userAgent) { - return this; - } - /** Does nothing. {@link RtspMediaSource} does not support error handling policies. */ @Override public Factory setLoadErrorHandlingPolicy( diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index ae65e5146d..503a84f2ae 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -52,7 +52,6 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestP import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; -import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; import com.google.android.exoplayer2.upstream.Loader; @@ -63,9 +62,9 @@ import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableList; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** A SmoothStreaming {@link MediaSource}. */ @@ -83,12 +82,10 @@ public final class SsMediaSource extends BaseMediaSource @Nullable private final DataSource.Factory manifestDataSourceFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private boolean usingCustomDrmSessionManagerProvider; private DrmSessionManagerProvider drmSessionManagerProvider; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long livePresentationDelayMs; @Nullable private ParsingLoadable.Parser manifestParser; - private List streamKeys; @Nullable private Object tag; /** @@ -119,7 +116,6 @@ public final class SsMediaSource extends BaseMediaSource loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); - streamKeys = Collections.emptyList(); } /** @@ -196,67 +192,13 @@ public final class SsMediaSource extends BaseMediaSource @Override public Factory setDrmSessionManagerProvider( @Nullable DrmSessionManagerProvider drmSessionManagerProvider) { - if (drmSessionManagerProvider != null) { - this.drmSessionManagerProvider = drmSessionManagerProvider; - this.usingCustomDrmSessionManagerProvider = true; - } else { - this.drmSessionManagerProvider = new DefaultDrmSessionManagerProvider(); - this.usingCustomDrmSessionManagerProvider = false; - } + this.drmSessionManagerProvider = + drmSessionManagerProvider != null + ? drmSessionManagerProvider + : new DefaultDrmSessionManagerProvider(); return this; } - @Deprecated - @Override - public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { - if (drmSessionManager == null) { - setDrmSessionManagerProvider(null); - } else { - setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager); - } - return this; - } - - @Deprecated - @Override - public Factory setDrmHttpDataSourceFactory( - @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { - if (!usingCustomDrmSessionManagerProvider) { - ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider) - .setDrmHttpDataSourceFactory(drmHttpDataSourceFactory); - } - return this; - } - - @Deprecated - @Override - public Factory setDrmUserAgent(@Nullable String userAgent) { - if (!usingCustomDrmSessionManagerProvider) { - ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider).setDrmUserAgent(userAgent); - } - return this; - } - - /** - * @deprecated Use {@link MediaItem.Builder#setStreamKeys(List)} and {@link - * #createMediaSource(MediaItem)} instead. - */ - @SuppressWarnings("deprecation") - @Deprecated - @Override - public Factory setStreamKeys(@Nullable List streamKeys) { - this.streamKeys = streamKeys != null ? streamKeys : Collections.emptyList(); - return this; - } - - /** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */ - @SuppressWarnings("deprecation") - @Deprecated - @Override - public SsMediaSource createMediaSource(Uri uri) { - return createMediaSource(new MediaItem.Builder().setUri(uri).build()); - } - /** * Returns a new {@link SsMediaSource} using the current parameters and the specified sideloaded * manifest. @@ -281,9 +223,9 @@ public final class SsMediaSource extends BaseMediaSource public SsMediaSource createMediaSource(SsManifest manifest, MediaItem mediaItem) { Assertions.checkArgument(!manifest.isLive); List streamKeys = - mediaItem.localConfiguration != null && !mediaItem.localConfiguration.streamKeys.isEmpty() + mediaItem.localConfiguration != null ? mediaItem.localConfiguration.streamKeys - : this.streamKeys; + : ImmutableList.of(); if (!streamKeys.isEmpty()) { manifest = manifest.copy(streamKeys); } @@ -295,7 +237,6 @@ public final class SsMediaSource extends BaseMediaSource .setMimeType(MimeTypes.APPLICATION_SS) .setUri(hasUri ? mediaItem.localConfiguration.uri : Uri.EMPTY) .setTag(hasTag ? mediaItem.localConfiguration.tag : tag) - .setStreamKeys(streamKeys) .build(); return new SsMediaSource( mediaItem, @@ -323,23 +264,13 @@ public final class SsMediaSource extends BaseMediaSource if (manifestParser == null) { manifestParser = new SsManifestParser(); } - List streamKeys = - !mediaItem.localConfiguration.streamKeys.isEmpty() - ? mediaItem.localConfiguration.streamKeys - : this.streamKeys; + List streamKeys = mediaItem.localConfiguration.streamKeys; if (!streamKeys.isEmpty()) { manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys); } - boolean needsTag = mediaItem.localConfiguration.tag == null && tag != null; - boolean needsStreamKeys = - mediaItem.localConfiguration.streamKeys.isEmpty() && !streamKeys.isEmpty(); - if (needsTag && needsStreamKeys) { - mediaItem = mediaItem.buildUpon().setTag(tag).setStreamKeys(streamKeys).build(); - } else if (needsTag) { + if (mediaItem.localConfiguration.tag == null && tag != null) { mediaItem = mediaItem.buildUpon().setTag(tag).build(); - } else if (needsStreamKeys) { - mediaItem = mediaItem.buildUpon().setStreamKeys(streamKeys).build(); } return new SsMediaSource( mediaItem, diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSourceTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSourceTest.java index a0719c0608..8bda74b25a 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSourceTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSourceTest.java @@ -20,9 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.upstream.FileDataSource; -import java.util.Collections; import org.junit.Test; import org.junit.runner.RunWith; @@ -65,45 +63,4 @@ public class SsMediaSourceTest { .isEqualTo(castNonNull(mediaItem.localConfiguration).uri); assertThat(ssMediaItem.localConfiguration.tag).isEqualTo(mediaItemTag); } - - // Tests backwards compatibility - @SuppressWarnings("deprecation") - @Test - public void factorySetStreamKeys_emptyMediaItemStreamKeys_setsMediaItemStreamKeys() { - MediaItem mediaItem = MediaItem.fromUri("http://www.google.com"); - StreamKey streamKey = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 1); - SsMediaSource.Factory factory = - new SsMediaSource.Factory(new FileDataSource.Factory()) - .setStreamKeys(Collections.singletonList(streamKey)); - - MediaItem ssMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(ssMediaItem.localConfiguration).isNotNull(); - assertThat(ssMediaItem.localConfiguration.uri) - .isEqualTo(castNonNull(mediaItem.localConfiguration).uri); - assertThat(ssMediaItem.localConfiguration.streamKeys).containsExactly(streamKey); - } - - // Tests backwards compatibility - @SuppressWarnings("deprecation") - @Test - public void factorySetStreamKeys_withMediaItemStreamKeys_doesNotOverrideMediaItemStreamKeys() { - StreamKey mediaItemStreamKey = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 1); - MediaItem mediaItem = - new MediaItem.Builder() - .setUri("http://www.google.com") - .setStreamKeys(Collections.singletonList(mediaItemStreamKey)) - .build(); - SsMediaSource.Factory factory = - new SsMediaSource.Factory(new FileDataSource.Factory()) - .setStreamKeys( - Collections.singletonList(new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 0))); - - MediaItem ssMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(ssMediaItem.localConfiguration).isNotNull(); - assertThat(ssMediaItem.localConfiguration.uri) - .isEqualTo(castNonNull(mediaItem.localConfiguration).uri); - assertThat(ssMediaItem.localConfiguration.streamKeys).containsExactly(mediaItemStreamKey); - } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactory.java index 0be568b2ef..b740de623e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactory.java @@ -18,13 +18,11 @@ package com.google.android.exoplayer2.testutil; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManagerProvider; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.util.Util; @@ -40,25 +38,6 @@ public class FakeMediaSourceFactory implements MediaSourceFactory { throw new UnsupportedOperationException(); } - @Deprecated - @Override - public MediaSourceFactory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { - throw new UnsupportedOperationException(); - } - - @Deprecated - @Override - public MediaSourceFactory setDrmHttpDataSourceFactory( - @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { - throw new UnsupportedOperationException(); - } - - @Deprecated - @Override - public MediaSourceFactory setDrmUserAgent(@Nullable String userAgent) { - throw new UnsupportedOperationException(); - } - @Override public MediaSourceFactory setLoadErrorHandlingPolicy( @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { From 4240da5966a9eab3416127914f3346090c7efb05 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Wed, 22 Dec 2021 11:16:53 +0000 Subject: [PATCH 39/56] Add TransformationRequest. PiperOrigin-RevId: 417786661 --- RELEASENOTES.md | 1 + .../RepeatedTranscodeTransformationTest.java | 22 +- .../transformer/mh/SefTransformationTest.java | 6 +- ...ransformationMatrixTransformationTest.java | 8 +- .../transformer/AudioSamplePipeline.java | 14 +- .../transformer/Transformation.java | 52 ---- .../transformer/TransformationRequest.java | 239 ++++++++++++++++++ .../exoplayer2/transformer/Transformer.java | 233 ++++++----------- .../transformer/TransformerAudioRenderer.java | 13 +- .../transformer/TransformerBaseRenderer.java | 14 +- .../transformer/TransformerVideoRenderer.java | 18 +- .../transformer/VideoSamplePipeline.java | 20 +- .../TransformationRequestTest.java | 49 ++++ .../transformer/TransformerBuilderTest.java | 32 +++ .../transformer/TransformerTest.java | 18 +- 15 files changed, 487 insertions(+), 252 deletions(-) delete mode 100644 library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformation.java create mode 100644 library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationRequest.java create mode 100644 library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformationRequestTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 85f9aec835..cfde3aa0ac 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -77,6 +77,7 @@ * Increase required min API version to 21. * `TransformationException` is now used to describe errors that occur during a transformation. + * Add `TransformationRequest` for specifying the transformation options. * MediaSession extension: * Remove deprecated call to `onStop(/* reset= */ true)` and provide an opt-out flag for apps that don't want to clear the playlist on stop. diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/RepeatedTranscodeTransformationTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/RepeatedTranscodeTransformationTest.java index 4b24c2b448..636d289f1e 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/RepeatedTranscodeTransformationTest.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/RepeatedTranscodeTransformationTest.java @@ -22,6 +22,7 @@ import android.content.Context; import android.graphics.Matrix; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.transformer.TransformationRequest; import com.google.android.exoplayer2.transformer.Transformer; import com.google.android.exoplayer2.util.MimeTypes; import java.util.HashSet; @@ -43,9 +44,12 @@ public final class RepeatedTranscodeTransformationTest { transformationMatrix.postTranslate((float) 0.1, (float) 0.1); Transformer transformer = new Transformer.Builder(context) - .setVideoMimeType(MimeTypes.VIDEO_H265) - .setTransformationMatrix(transformationMatrix) - .setAudioMimeType(MimeTypes.AUDIO_AMR_NB) + .setTransformationRequest( + new TransformationRequest.Builder() + .setVideoMimeType(MimeTypes.VIDEO_H265) + .setTransformationMatrix(transformationMatrix) + .setAudioMimeType(MimeTypes.AUDIO_AMR_NB) + .build()) .build(); Set differentOutputSizesBytes = new HashSet<>(); @@ -74,9 +78,12 @@ public final class RepeatedTranscodeTransformationTest { transformationMatrix.postTranslate((float) 0.1, (float) 0.1); Transformer transformer = new Transformer.Builder(context) - .setVideoMimeType(MimeTypes.VIDEO_H265) - .setTransformationMatrix(transformationMatrix) .setRemoveAudio(true) + .setTransformationRequest( + new TransformationRequest.Builder() + .setVideoMimeType(MimeTypes.VIDEO_H265) + .setTransformationMatrix(transformationMatrix) + .build()) .build(); Set differentOutputSizesBytes = new HashSet<>(); @@ -103,8 +110,11 @@ public final class RepeatedTranscodeTransformationTest { Context context = ApplicationProvider.getApplicationContext(); Transformer transcodingTransformer = new Transformer.Builder(context) - .setAudioMimeType(MimeTypes.AUDIO_AMR_NB) .setRemoveVideo(true) + .setTransformationRequest( + new TransformationRequest.Builder() + .setAudioMimeType(MimeTypes.AUDIO_AMR_NB) + .build()) .build(); Set differentOutputSizesBytes = new HashSet<>(); diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/SefTransformationTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/SefTransformationTest.java index 129e230694..092a824b0e 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/SefTransformationTest.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/SefTransformationTest.java @@ -21,6 +21,7 @@ import static com.google.android.exoplayer2.transformer.mh.AndroidTestUtil.runTr import android.content.Context; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.transformer.TransformationRequest; import com.google.android.exoplayer2.transformer.Transformer; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,7 +33,10 @@ public class SefTransformationTest { public void sefTransform() throws Exception { Context context = ApplicationProvider.getApplicationContext(); Transformer transformer = - new Transformer.Builder(context).setFlattenForSlowMotion(true).build(); + new Transformer.Builder(context) + .setTransformationRequest( + new TransformationRequest.Builder().setFlattenForSlowMotion(true).build()) + .build(); runTransformer( context, /* testId = */ "sefTransform", diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/SetTransformationMatrixTransformationTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/SetTransformationMatrixTransformationTest.java index d2ebe9b491..59e2219e35 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/SetTransformationMatrixTransformationTest.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/SetTransformationMatrixTransformationTest.java @@ -22,6 +22,7 @@ import android.content.Context; import android.graphics.Matrix; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.transformer.TransformationRequest; import com.google.android.exoplayer2.transformer.Transformer; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,7 +36,12 @@ public class SetTransformationMatrixTransformationTest { Matrix transformationMatrix = new Matrix(); transformationMatrix.postTranslate(/* dx= */ .2f, /* dy= */ .1f); Transformer transformer = - new Transformer.Builder(context).setTransformationMatrix(transformationMatrix).build(); + new Transformer.Builder(context) + .setTransformationRequest( + new TransformationRequest.Builder() + .setTransformationMatrix(transformationMatrix) + .build()) + .build(); runTransformer( context, diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java index 886f38379e..e9cf77f008 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java @@ -43,7 +43,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024; private final Format inputFormat; - private final Transformation transformation; + private final TransformationRequest transformationRequest; private final Codec.EncoderFactory encoderFactory; private final Codec decoder; @@ -66,12 +66,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; public AudioSamplePipeline( Format inputFormat, - Transformation transformation, + TransformationRequest transformationRequest, Codec.EncoderFactory encoderFactory, Codec.DecoderFactory decoderFactory) throws TransformationException { this.inputFormat = inputFormat; - this.transformation = transformation; + this.transformationRequest = transformationRequest; this.encoderFactory = encoderFactory; decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); @@ -294,7 +294,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; decoderOutputFormat.sampleRate, decoderOutputFormat.channelCount, decoderOutputFormat.pcmEncoding); - if (transformation.flattenForSlowMotion) { + if (transformationRequest.flattenForSlowMotion) { try { outputAudioFormat = sonicAudioProcessor.configure(outputAudioFormat); flushSonicAndSetSpeed(currentSpeed); @@ -310,9 +310,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; encoderFactory.createForAudioEncoding( new Format.Builder() .setSampleMimeType( - transformation.audioMimeType == null + transformationRequest.audioMimeType == null ? inputFormat.sampleMimeType - : transformation.audioMimeType) + : transformationRequest.audioMimeType) .setSampleRate(outputAudioFormat.sampleRate) .setChannelCount(outputAudioFormat.channelCount) .setAverageBitrate(DEFAULT_ENCODER_BITRATE) @@ -322,7 +322,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } private boolean isSpeedChanging(BufferInfo bufferInfo) { - if (!transformation.flattenForSlowMotion) { + if (!transformationRequest.flattenForSlowMotion) { return false; } float newSpeed = speedProvider.getSpeed(bufferInfo.presentationTimeUs); diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformation.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformation.java deleted file mode 100644 index c151f177ab..0000000000 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformation.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2020 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 android.graphics.Matrix; -import androidx.annotation.Nullable; - -/** A media transformation configuration. */ -/* package */ final class Transformation { - - public final boolean removeAudio; - public final boolean removeVideo; - public final boolean flattenForSlowMotion; - public final int outputHeight; - public final Matrix transformationMatrix; - public final String containerMimeType; - @Nullable public final String audioMimeType; - @Nullable public final String videoMimeType; - - public Transformation( - boolean removeAudio, - boolean removeVideo, - boolean flattenForSlowMotion, - int outputHeight, - Matrix transformationMatrix, - String containerMimeType, - @Nullable String audioMimeType, - @Nullable String videoMimeType) { - this.removeAudio = removeAudio; - this.removeVideo = removeVideo; - this.flattenForSlowMotion = flattenForSlowMotion; - this.outputHeight = outputHeight; - this.transformationMatrix = transformationMatrix; - this.containerMimeType = containerMimeType; - this.audioMimeType = audioMimeType; - this.videoMimeType = videoMimeType; - } -} diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationRequest.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationRequest.java new file mode 100644 index 0000000000..823eaf3fb1 --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationRequest.java @@ -0,0 +1,239 @@ +/* + * Copyright 2020 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 android.graphics.Matrix; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; +import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; + +/** A media transformation request. */ +public final class TransformationRequest { + + /** A builder for {@link TransformationRequest} instances. */ + public static final class Builder { + + private Matrix transformationMatrix; + private boolean flattenForSlowMotion; + private int outputHeight; + @Nullable private String audioMimeType; + @Nullable private String videoMimeType; + + /** + * Creates a new instance with default values. + * + *

          Use {@link TransformationRequest#buildUpon()} to obtain a builder representing an existing + * {@link TransformationRequest}. + */ + public Builder() { + transformationMatrix = new Matrix(); + outputHeight = C.LENGTH_UNSET; + } + + private Builder(TransformationRequest transformationRequest) { + this.transformationMatrix = transformationRequest.transformationMatrix; + this.flattenForSlowMotion = transformationRequest.flattenForSlowMotion; + this.outputHeight = transformationRequest.outputHeight; + this.audioMimeType = transformationRequest.audioMimeType; + this.videoMimeType = transformationRequest.videoMimeType; + } + + /** + * Sets the transformation matrix. The default value is to apply no change. + * + *

          This can be used to perform operations supported by {@link Matrix}, like scaling and + * rotating the video. + * + *

          For now, resolution will not be affected by this method. + * + * @param transformationMatrix The transformation to apply to video frames. + * @return This builder. + */ + public Builder setTransformationMatrix(Matrix transformationMatrix) { + // TODO(b/201293185): After {@link #setResolution} supports arbitrary resolutions, + // allow transformations to change the resolution, by scaling to the appropriate min/max + // values. This will also be required to create the VertexTransformation class, in order to + // have aspect ratio helper methods (which require resolution to change). + this.transformationMatrix = transformationMatrix; + return this; + } + + /** + * Sets whether the input should be flattened for media containing slow motion markers. The + * transformed output is obtained by removing the slow motion metadata and by actually slowing + * down the parts of the video and audio streams defined in this metadata. The default value for + * {@code flattenForSlowMotion} is {@code false}. + * + *

          Only Samsung Extension Format (SEF) slow motion metadata type is supported. The + * transformation has no effect if the input does not contain this metadata type. + * + *

          For SEF slow motion media, the following assumptions are made on the input: + * + *

            + *
          • The input container format is (unfragmented) MP4. + *
          • The input contains an AVC video elementary stream with temporal SVC. + *
          • The recording frame rate of the video is 120 or 240 fps. + *
          + * + *

          If specifying a {@link MediaSourceFactory} using {@link + * Transformer.Builder#setMediaSourceFactory(MediaSourceFactory)}, make sure that {@link + * Mp4Extractor#FLAG_READ_SEF_DATA} is set on the {@link Mp4Extractor} used. Otherwise, the slow + * motion metadata will be ignored and the input won't be flattened. + * + * @param flattenForSlowMotion Whether to flatten for slow motion. + * @return This builder. + */ + public Builder setFlattenForSlowMotion(boolean flattenForSlowMotion) { + this.flattenForSlowMotion = flattenForSlowMotion; + return this; + } + + /** + * Sets the output resolution using the output height. The default value is the same height as + * the input. Output width will scale to preserve the input video's aspect ratio. + * + *

          For now, only "popular" heights like 144, 240, 360, 480, 720, 1080, 1440, or 2160 are + * supported, to ensure compatibility on different devices. + * + *

          For example, a 1920x1440 video can be scaled to 640x480 by calling setResolution(480). + * + * @param outputHeight The output height in pixels. + * @return This builder. + */ + public Builder setResolution(int outputHeight) { + // TODO(b/201293185): Restructure to input a Presentation class. + // TODO(b/201293185): Check encoder codec capabilities in order to allow arbitrary + // resolutions and reasonable fallbacks. + if (outputHeight != 144 + && outputHeight != 240 + && outputHeight != 360 + && outputHeight != 480 + && outputHeight != 720 + && outputHeight != 1080 + && outputHeight != 1440 + && outputHeight != 2160) { + throw new IllegalArgumentException( + "Please use a height of 144, 240, 360, 480, 720, 1080, 1440, or 2160."); + } + this.outputHeight = outputHeight; + return this; + } + + /** + * Sets the video MIME type of the output. The default value is to use the same MIME type as the + * input. Supported values are: + * + *

            + *
          • {@link MimeTypes#VIDEO_H263} + *
          • {@link MimeTypes#VIDEO_H264} + *
          • {@link MimeTypes#VIDEO_H265} from API level 24 + *
          • {@link MimeTypes#VIDEO_MP4V} + *
          + * + * @param videoMimeType The MIME type of the video samples in the output. + * @return This builder. + */ + public Builder setVideoMimeType(String videoMimeType) { + // TODO(b/209469847): Validate videoMimeType here once deprecated + // Transformer.Builder#setOuputMimeType(String) has been removed. + this.videoMimeType = videoMimeType; + return this; + } + + /** + * Sets the audio MIME type of the output. The default value is to use the same MIME type as the + * input. Supported values are: + * + *
            + *
          • {@link MimeTypes#AUDIO_AAC} + *
          • {@link MimeTypes#AUDIO_AMR_NB} + *
          • {@link MimeTypes#AUDIO_AMR_WB} + *
          + * + * @param audioMimeType The MIME type of the audio samples in the output. + * @return This builder. + */ + public Builder setAudioMimeType(String audioMimeType) { + // TODO(b/209469847): Validate audioMimeType here once deprecated + // Transformer.Builder#setOuputMimeType(String) has been removed. + this.audioMimeType = audioMimeType; + return this; + } + + /** Builds a {@link TransformationRequest} instance. */ + public TransformationRequest build() { + return new TransformationRequest( + transformationMatrix, flattenForSlowMotion, outputHeight, audioMimeType, videoMimeType); + } + } + + public final Matrix transformationMatrix; + public final boolean flattenForSlowMotion; + public final int outputHeight; + @Nullable public final String audioMimeType; + @Nullable public final String videoMimeType; + + private TransformationRequest( + Matrix transformationMatrix, + boolean flattenForSlowMotion, + int outputHeight, + @Nullable String audioMimeType, + @Nullable String videoMimeType) { + this.transformationMatrix = transformationMatrix; + this.flattenForSlowMotion = flattenForSlowMotion; + this.outputHeight = outputHeight; + this.audioMimeType = audioMimeType; + this.videoMimeType = videoMimeType; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TransformationRequest)) { + return false; + } + TransformationRequest that = (TransformationRequest) o; + return transformationMatrix.equals(that.transformationMatrix) + && flattenForSlowMotion == that.flattenForSlowMotion + && outputHeight == that.outputHeight + && Util.areEqual(audioMimeType, that.audioMimeType) + && Util.areEqual(videoMimeType, that.videoMimeType); + } + + @Override + public int hashCode() { + int result = transformationMatrix.hashCode(); + result = 31 * result + (flattenForSlowMotion ? 1 : 0); + result = 31 * result + outputHeight; + result = 31 * result + (audioMimeType != null ? audioMimeType.hashCode() : 0); + result = 31 * result + (videoMimeType != null ? videoMimeType.hashCode() : 0); + return result; + } + + /** + * Returns a new {@link TransformationRequest.Builder} initialized with the values of this + * instance. + */ + public Builder buildUpon() { + return new Builder(this); + } +} diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java index b310db74e4..636a137d52 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java @@ -97,12 +97,9 @@ public final class Transformer { private Muxer.Factory muxerFactory; private boolean removeAudio; private boolean removeVideo; - private boolean flattenForSlowMotion; - private int outputHeight; - private Matrix transformationMatrix; private String containerMimeType; - @Nullable private String audioMimeType; - @Nullable private String videoMimeType; + // TODO(b/204869912): Make final once deprecated setters are removed. + private TransformationRequest transformationRequest; private Transformer.Listener listener; private DebugViewProvider debugViewProvider; private Looper looper; @@ -113,14 +110,13 @@ public final class Transformer { @Deprecated public Builder() { muxerFactory = new FrameworkMuxer.Factory(); - outputHeight = C.LENGTH_UNSET; - transformationMatrix = new Matrix(); - containerMimeType = MimeTypes.VIDEO_MP4; listener = new Listener() {}; looper = Util.getCurrentOrMainLooper(); clock = Clock.DEFAULT; encoderFactory = Codec.EncoderFactory.DEFAULT; debugViewProvider = DebugViewProvider.NONE; + containerMimeType = MimeTypes.VIDEO_MP4; + transformationRequest = new TransformationRequest.Builder().build(); } /** @@ -131,14 +127,13 @@ public final class Transformer { public Builder(Context context) { this.context = context.getApplicationContext(); muxerFactory = new FrameworkMuxer.Factory(); - outputHeight = C.LENGTH_UNSET; - transformationMatrix = new Matrix(); - containerMimeType = MimeTypes.VIDEO_MP4; listener = new Listener() {}; looper = Util.getCurrentOrMainLooper(); clock = Clock.DEFAULT; encoderFactory = Codec.EncoderFactory.DEFAULT; debugViewProvider = DebugViewProvider.NONE; + containerMimeType = MimeTypes.VIDEO_MP4; + this.transformationRequest = new TransformationRequest.Builder().build(); } /** Creates a builder with the values of the provided {@link Transformer}. */ @@ -146,14 +141,10 @@ public final class Transformer { this.context = transformer.context; this.mediaSourceFactory = transformer.mediaSourceFactory; this.muxerFactory = transformer.muxerFactory; - this.removeAudio = transformer.transformation.removeAudio; - this.removeVideo = transformer.transformation.removeVideo; - this.flattenForSlowMotion = transformer.transformation.flattenForSlowMotion; - this.outputHeight = transformer.transformation.outputHeight; - this.transformationMatrix = transformer.transformation.transformationMatrix; - this.containerMimeType = transformer.transformation.containerMimeType; - this.audioMimeType = transformer.transformation.audioMimeType; - this.videoMimeType = transformer.transformation.videoMimeType; + this.removeAudio = transformer.removeAudio; + this.removeVideo = transformer.removeVideo; + this.containerMimeType = transformer.containerMimeType; + this.transformationRequest = transformer.transformationRequest; this.listener = transformer.listener; this.looper = transformer.looper; this.encoderFactory = transformer.encoderFactory; @@ -168,6 +159,17 @@ public final class Transformer { return this; } + /** + * Sets the {@link TransformationRequest} which configures the editing and transcoding options. + * + * @param transformationRequest The {@link TransformationRequest}. + * @return This builder. + */ + public Builder setTransformationRequest(TransformationRequest transformationRequest) { + this.transformationRequest = transformationRequest; + return this; + } + /** * Sets the {@link MediaSourceFactory} to be used to retrieve the inputs to transform. The * default value is a {@link DefaultMediaSourceFactory} built with the context provided in @@ -210,83 +212,31 @@ public final class Transformer { } /** - * Sets whether the input should be flattened for media containing slow motion markers. The - * transformed output is obtained by removing the slow motion metadata and by actually slowing - * down the parts of the video and audio streams defined in this metadata. The default value for - * {@code flattenForSlowMotion} is {@code false}. - * - *

          Only Samsung Extension Format (SEF) slow motion metadata type is supported. The - * transformation has no effect if the input does not contain this metadata type. - * - *

          For SEF slow motion media, the following assumptions are made on the input: - * - *

            - *
          • The input container format is (unfragmented) MP4. - *
          • The input contains an AVC video elementary stream with temporal SVC. - *
          • The recording frame rate of the video is 120 or 240 fps. - *
          - * - *

          If specifying a {@link MediaSourceFactory} using {@link - * #setMediaSourceFactory(MediaSourceFactory)}, make sure that {@link - * Mp4Extractor#FLAG_READ_SEF_DATA} is set on the {@link Mp4Extractor} used. Otherwise, the slow - * motion metadata will be ignored and the input won't be flattened. - * - * @param flattenForSlowMotion Whether to flatten for slow motion. - * @return This builder. + * @deprecated Use {@link TransformationRequest.Builder#setFlattenForSlowMotion(boolean)} + * instead. */ + @Deprecated public Builder setFlattenForSlowMotion(boolean flattenForSlowMotion) { - this.flattenForSlowMotion = flattenForSlowMotion; + transformationRequest = + transformationRequest.buildUpon().setFlattenForSlowMotion(flattenForSlowMotion).build(); return this; } - /** - * Sets the output resolution using the output height. The default value is the same height as - * the input. Output width will scale to preserve the input video's aspect ratio. - * - *

          For now, only "popular" heights like 144, 240, 360, 480, 720, 1080, 1440, or 2160 are - * supported, to ensure compatibility on different devices. - * - *

          For example, a 1920x1440 video can be scaled to 640x480 by calling setResolution(480). - * - * @param outputHeight The output height in pixels. - * @return This builder. - */ + /** @deprecated Use {@link TransformationRequest.Builder#setResolution(int)} instead. */ + @Deprecated public Builder setResolution(int outputHeight) { - // TODO(Internal b/201293185): Restructure to input a Presentation class. - // TODO(Internal b/201293185): Check encoder codec capabilities in order to allow arbitrary - // resolutions and reasonable fallbacks. - if (outputHeight != 144 - && outputHeight != 240 - && outputHeight != 360 - && outputHeight != 480 - && outputHeight != 720 - && outputHeight != 1080 - && outputHeight != 1440 - && outputHeight != 2160) { - throw new IllegalArgumentException( - "Please use a height of 144, 240, 360, 480, 720, 1080, 1440, or 2160."); - } - this.outputHeight = outputHeight; + transformationRequest = transformationRequest.buildUpon().setResolution(outputHeight).build(); return this; } /** - * Sets the transformation matrix. The default value is to apply no change. - * - *

          This can be used to perform operations supported by {@link Matrix}, like scaling and - * rotating the video. - * - *

          For now, resolution will not be affected by this method. - * - * @param transformationMatrix The transformation to apply to video frames. - * @return This builder. + * @deprecated Use {@link TransformationRequest.Builder#setTransformationMatrix(Matrix)} + * instead. */ + @Deprecated public Builder setTransformationMatrix(Matrix transformationMatrix) { - // TODO(Internal b/201293185): After {@link #setResolution} supports arbitrary resolutions, - // allow transformations to change the resolution, by scaling to the appropriate min/max - // values. This will also be required to create the VertexTransformation class, in order to - // have aspect ratio helper methods (which require resolution to change). - this.transformationMatrix = transformationMatrix; + transformationRequest = + transformationRequest.buildUpon().setTransformationMatrix(transformationMatrix).build(); return this; } @@ -300,40 +250,19 @@ public final class Transformer { return this; } - /** - * Sets the video MIME type of the output. The default value is to use the same MIME type as the - * input. Supported values are: - * - *

            - *
          • {@link MimeTypes#VIDEO_H263} - *
          • {@link MimeTypes#VIDEO_H264} - *
          • {@link MimeTypes#VIDEO_H265} from API level 24 - *
          • {@link MimeTypes#VIDEO_MP4V} - *
          - * - * @param videoMimeType The MIME type of the video samples in the output. - * @return This builder. - */ + /** @deprecated Use {@link TransformationRequest.Builder#setVideoMimeType(String)} instead. */ + @Deprecated public Builder setVideoMimeType(String videoMimeType) { - this.videoMimeType = videoMimeType; + transformationRequest = + transformationRequest.buildUpon().setVideoMimeType(videoMimeType).build(); return this; } - /** - * Sets the audio MIME type of the output. The default value is to use the same MIME type as the - * input. Supported values are: - * - *
            - *
          • {@link MimeTypes#AUDIO_AAC} - *
          • {@link MimeTypes#AUDIO_AMR_NB} - *
          • {@link MimeTypes#AUDIO_AMR_WB} - *
          - * - * @param audioMimeType The MIME type of the audio samples in the output. - * @return This builder. - */ + /** @deprecated Use {@link TransformationRequest.Builder#setAudioMimeType(String)} instead. */ + @Deprecated public Builder setAudioMimeType(String audioMimeType) { - this.audioMimeType = audioMimeType; + transformationRequest = + transformationRequest.buildUpon().setAudioMimeType(audioMimeType).build(); return this; } @@ -432,7 +361,7 @@ public final class Transformer { checkNotNull(context); if (mediaSourceFactory == null) { DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); - if (flattenForSlowMotion) { + if (transformationRequest.flattenForSlowMotion) { defaultExtractorsFactory.setMp4ExtractorFlags(Mp4Extractor.FLAG_READ_SEF_DATA); } mediaSourceFactory = new DefaultMediaSourceFactory(context, defaultExtractorsFactory); @@ -440,27 +369,20 @@ public final class Transformer { checkState( muxerFactory.supportsOutputMimeType(containerMimeType), "Unsupported container MIME type: " + containerMimeType); - if (audioMimeType != null) { - checkSampleMimeType(audioMimeType); + if (transformationRequest.audioMimeType != null) { + checkSampleMimeType(transformationRequest.audioMimeType); } - if (videoMimeType != null) { - checkSampleMimeType(videoMimeType); + if (transformationRequest.videoMimeType != null) { + checkSampleMimeType(transformationRequest.videoMimeType); } - Transformation transformation = - new Transformation( - removeAudio, - removeVideo, - flattenForSlowMotion, - outputHeight, - transformationMatrix, - containerMimeType, - audioMimeType, - videoMimeType); return new Transformer( context, mediaSourceFactory, muxerFactory, - transformation, + removeAudio, + removeVideo, + containerMimeType, + transformationRequest, listener, looper, clock, @@ -551,7 +473,10 @@ public final class Transformer { private final Context context; private final MediaSourceFactory mediaSourceFactory; private final Muxer.Factory muxerFactory; - private final Transformation transformation; + private final boolean removeAudio; + private final boolean removeVideo; + private final String containerMimeType; + private final TransformationRequest transformationRequest; private final Looper looper; private final Clock clock; private final Codec.EncoderFactory encoderFactory; @@ -567,20 +492,24 @@ public final class Transformer { Context context, MediaSourceFactory mediaSourceFactory, Muxer.Factory muxerFactory, - Transformation transformation, + boolean removeAudio, + boolean removeVideo, + String containerMimeType, + TransformationRequest transformationRequest, Transformer.Listener listener, Looper looper, Clock clock, Codec.EncoderFactory encoderFactory, Codec.DecoderFactory decoderFactory, Transformer.DebugViewProvider debugViewProvider) { - checkState( - !transformation.removeAudio || !transformation.removeVideo, - "Audio and video cannot both be removed."); + checkState(!removeAudio || !removeVideo, "Audio and video cannot both be removed."); this.context = context; this.mediaSourceFactory = mediaSourceFactory; this.muxerFactory = muxerFactory; - this.transformation = transformation; + this.removeAudio = removeAudio; + this.removeVideo = removeVideo; + this.containerMimeType = containerMimeType; + this.transformationRequest = transformationRequest; this.listener = listener; this.looper = looper; this.clock = clock; @@ -626,7 +555,7 @@ public final class Transformer { * @throws IOException If an error occurs opening the output file for writing. */ public void startTransformation(MediaItem mediaItem, String path) throws IOException { - startTransformation(mediaItem, muxerFactory.create(path, transformation.containerMimeType)); + startTransformation(mediaItem, muxerFactory.create(path, containerMimeType)); } /** @@ -654,8 +583,7 @@ public final class Transformer { @RequiresApi(26) public void startTransformation(MediaItem mediaItem, ParcelFileDescriptor parcelFileDescriptor) throws IOException { - startTransformation( - mediaItem, muxerFactory.create(parcelFileDescriptor, transformation.containerMimeType)); + startTransformation(mediaItem, muxerFactory.create(parcelFileDescriptor, containerMimeType)); } private void startTransformation(MediaItem mediaItem, Muxer muxer) { @@ -664,8 +592,7 @@ public final class Transformer { throw new IllegalStateException("There is already a transformation in progress."); } - MuxerWrapper muxerWrapper = - new MuxerWrapper(muxer, muxerFactory, transformation.containerMimeType); + MuxerWrapper muxerWrapper = new MuxerWrapper(muxer, muxerFactory, containerMimeType); this.muxerWrapper = muxerWrapper; DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); trackSelector.setParameters( @@ -688,7 +615,9 @@ public final class Transformer { new TransformerRenderersFactory( context, muxerWrapper, - transformation, + removeAudio, + removeVideo, + transformationRequest, encoderFactory, decoderFactory, debugViewProvider)) @@ -779,7 +708,9 @@ public final class Transformer { private final Context context; private final MuxerWrapper muxerWrapper; private final TransformerMediaClock mediaClock; - private final Transformation transformation; + private final boolean removeAudio; + private final boolean removeVideo; + private final TransformationRequest transformationRequest; private final Codec.EncoderFactory encoderFactory; private final Codec.DecoderFactory decoderFactory; private final Transformer.DebugViewProvider debugViewProvider; @@ -787,13 +718,17 @@ public final class Transformer { public TransformerRenderersFactory( Context context, MuxerWrapper muxerWrapper, - Transformation transformation, + boolean removeAudio, + boolean removeVideo, + TransformationRequest transformationRequest, Codec.EncoderFactory encoderFactory, Codec.DecoderFactory decoderFactory, Transformer.DebugViewProvider debugViewProvider) { this.context = context; this.muxerWrapper = muxerWrapper; - this.transformation = transformation; + this.removeAudio = removeAudio; + this.removeVideo = removeVideo; + this.transformationRequest = transformationRequest; this.encoderFactory = encoderFactory; this.decoderFactory = decoderFactory; this.debugViewProvider = debugViewProvider; @@ -807,22 +742,22 @@ public final class Transformer { AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput, MetadataOutput metadataRendererOutput) { - int rendererCount = transformation.removeAudio || transformation.removeVideo ? 1 : 2; + int rendererCount = removeAudio || removeVideo ? 1 : 2; Renderer[] renderers = new Renderer[rendererCount]; int index = 0; - if (!transformation.removeAudio) { + if (!removeAudio) { renderers[index] = new TransformerAudioRenderer( - muxerWrapper, mediaClock, transformation, encoderFactory, decoderFactory); + muxerWrapper, mediaClock, transformationRequest, encoderFactory, decoderFactory); index++; } - if (!transformation.removeVideo) { + if (!removeVideo) { renderers[index] = new TransformerVideoRenderer( context, muxerWrapper, mediaClock, - transformation, + transformationRequest, encoderFactory, decoderFactory, debugViewProvider); diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java index 1e25726f83..b09a80404a 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java @@ -39,10 +39,10 @@ import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; public TransformerAudioRenderer( MuxerWrapper muxerWrapper, TransformerMediaClock mediaClock, - Transformation transformation, + TransformationRequest transformationRequest, Codec.EncoderFactory encoderFactory, Codec.DecoderFactory decoderFactory) { - super(C.TRACK_TYPE_AUDIO, muxerWrapper, mediaClock, transformation); + super(C.TRACK_TYPE_AUDIO, muxerWrapper, mediaClock, transformationRequest); this.encoderFactory = encoderFactory; this.decoderFactory = decoderFactory; decoderInputBuffer = @@ -69,7 +69,8 @@ import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; Format inputFormat = checkNotNull(formatHolder.format); if (shouldTranscode(inputFormat)) { samplePipeline = - new AudioSamplePipeline(inputFormat, transformation, encoderFactory, decoderFactory); + new AudioSamplePipeline( + inputFormat, transformationRequest, encoderFactory, decoderFactory); } else { samplePipeline = new PassthroughSamplePipeline(inputFormat); } @@ -77,11 +78,11 @@ import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; } private boolean shouldTranscode(Format inputFormat) { - if (transformation.audioMimeType != null - && !transformation.audioMimeType.equals(inputFormat.sampleMimeType)) { + if (transformationRequest.audioMimeType != null + && !transformationRequest.audioMimeType.equals(inputFormat.sampleMimeType)) { return true; } - if (transformation.flattenForSlowMotion && isSlowMotion(inputFormat)) { + if (transformationRequest.flattenForSlowMotion && isSlowMotion(inputFormat)) { return true; } return false; diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java index ba36131f85..4dd8ed7775 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java @@ -38,7 +38,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; protected final MuxerWrapper muxerWrapper; protected final TransformerMediaClock mediaClock; - protected final Transformation transformation; + protected final TransformationRequest transformationRequest; protected boolean isRendererStarted; protected boolean muxerWrapperTrackAdded; @@ -50,11 +50,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; int trackType, MuxerWrapper muxerWrapper, TransformerMediaClock mediaClock, - Transformation transformation) { + TransformationRequest transformationRequest) { super(trackType); this.muxerWrapper = muxerWrapper; this.mediaClock = mediaClock; - this.transformation = transformation; + this.transformationRequest = transformationRequest; } @Override @@ -65,14 +65,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE); } else if ((MimeTypes.isAudio(sampleMimeType) && muxerWrapper.supportsSampleMimeType( - transformation.audioMimeType == null + transformationRequest.audioMimeType == null ? sampleMimeType - : transformation.audioMimeType)) + : transformationRequest.audioMimeType)) || (MimeTypes.isVideo(sampleMimeType) && muxerWrapper.supportsSampleMimeType( - transformation.videoMimeType == null + transformationRequest.videoMimeType == null ? sampleMimeType - : transformation.videoMimeType))) { + : transformationRequest.videoMimeType))) { return RendererCapabilities.create(C.FORMAT_HANDLED); } else { return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE); diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java index ec062bfeb9..bec419513e 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java @@ -45,11 +45,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; Context context, MuxerWrapper muxerWrapper, TransformerMediaClock mediaClock, - Transformation transformation, + TransformationRequest transformationRequest, Codec.EncoderFactory encoderFactory, Codec.DecoderFactory decoderFactory, Transformer.DebugViewProvider debugViewProvider) { - super(C.TRACK_TYPE_VIDEO, muxerWrapper, mediaClock, transformation); + super(C.TRACK_TYPE_VIDEO, muxerWrapper, mediaClock, transformationRequest); this.context = context; this.encoderFactory = encoderFactory; this.decoderFactory = decoderFactory; @@ -81,29 +81,29 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; new VideoSamplePipeline( context, inputFormat, - transformation, + transformationRequest, encoderFactory, decoderFactory, debugViewProvider); } else { samplePipeline = new PassthroughSamplePipeline(inputFormat); } - if (transformation.flattenForSlowMotion) { + if (transformationRequest.flattenForSlowMotion) { sefSlowMotionFlattener = new SefSlowMotionFlattener(inputFormat); } return true; } private boolean shouldTranscode(Format inputFormat) { - if (transformation.videoMimeType != null - && !transformation.videoMimeType.equals(inputFormat.sampleMimeType)) { + if (transformationRequest.videoMimeType != null + && !transformationRequest.videoMimeType.equals(inputFormat.sampleMimeType)) { return true; } - if (transformation.outputHeight != C.LENGTH_UNSET - && transformation.outputHeight != inputFormat.height) { + if (transformationRequest.outputHeight != C.LENGTH_UNSET + && transformationRequest.outputHeight != inputFormat.height) { return true; } - if (!transformation.transformationMatrix.isIdentity()) { + if (!transformationRequest.transformationMatrix.isIdentity()) { return true; } return false; diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java index d23d58b4ca..d8f83d4aa0 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java @@ -50,7 +50,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public VideoSamplePipeline( Context context, Format inputFormat, - Transformation transformation, + TransformationRequest transformationRequest, Codec.EncoderFactory encoderFactory, Codec.DecoderFactory decoderFactory, Transformer.DebugViewProvider debugViewProvider) @@ -63,10 +63,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // TODO(internal b/209781577): Think about which edge length should be set for portrait videos. int outputWidth = inputFormat.width; int outputHeight = inputFormat.height; - if (transformation.outputHeight != C.LENGTH_UNSET - && transformation.outputHeight != inputFormat.height) { - outputWidth = inputFormat.width * transformation.outputHeight / inputFormat.height; - outputHeight = transformation.outputHeight; + if (transformationRequest.outputHeight != C.LENGTH_UNSET + && transformationRequest.outputHeight != inputFormat.height) { + outputWidth = inputFormat.width * transformationRequest.outputHeight / inputFormat.height; + outputHeight = transformationRequest.outputHeight; } if (inputFormat.height > inputFormat.width) { @@ -83,7 +83,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // them back for improved encoder compatibility. // TODO(internal b/201293185): After fragment shader transformations are implemented, put // postrotation in a later vertex shader. - transformation.transformationMatrix.postRotate(outputRotationDegrees); + transformationRequest.transformationMatrix.postRotate(outputRotationDegrees); encoder = encoderFactory.createForVideoEncoding( @@ -92,19 +92,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; .setHeight(outputHeight) .setRotationDegrees(0) .setSampleMimeType( - transformation.videoMimeType != null - ? transformation.videoMimeType + transformationRequest.videoMimeType != null + ? transformationRequest.videoMimeType : inputFormat.sampleMimeType) .build()); if (inputFormat.height != outputHeight || inputFormat.width != outputWidth - || !transformation.transformationMatrix.isIdentity()) { + || !transformationRequest.transformationMatrix.isIdentity()) { frameEditor = FrameEditor.create( context, outputWidth, outputHeight, - transformation.transformationMatrix, + transformationRequest.transformationMatrix, /* outputSurface= */ checkNotNull(encoder.getInputSurface()), debugViewProvider); } diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformationRequestTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformationRequestTest.java new file mode 100644 index 0000000000..3a9a9db240 --- /dev/null +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformationRequestTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 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.common.truth.Truth.assertThat; + +import android.graphics.Matrix; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.util.MimeTypes; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link TransformationRequest}. */ +@RunWith(AndroidJUnit4.class) +public class TransformationRequestTest { + + @Test + public void buildUponTransformationRequest_createsEqualTransformationRequest() { + TransformationRequest request = createTestTransformationRequest(); + assertThat(request.buildUpon().build()).isEqualTo(request); + } + + private static TransformationRequest createTestTransformationRequest() { + Matrix transformationMatrix = new Matrix(); + transformationMatrix.preRotate(36); + transformationMatrix.postTranslate((float) 0.5, (float) -0.2); + + return new TransformationRequest.Builder() + .setFlattenForSlowMotion(true) + .setAudioMimeType(MimeTypes.AUDIO_AAC) + .setVideoMimeType(MimeTypes.VIDEO_H264) + .setTransformationMatrix(transformationMatrix) + .build(); + } +} diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerBuilderTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerBuilderTest.java index 8796678596..bff87ff35e 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerBuilderTest.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerBuilderTest.java @@ -51,4 +51,36 @@ public class TransformerBuilderTest { IllegalStateException.class, () -> new Transformer.Builder(context).setRemoveAudio(true).setRemoveVideo(true).build()); } + + // TODO(b/209469847): Move this test to TransformationRequestBuilderTest once deprecated + // Transformer.Builder#setOuputMimeType(String) has been removed. + @Test + public void build_withUnsupportedAudioMimeType_throws() { + Context context = ApplicationProvider.getApplicationContext(); + TransformationRequest transformationRequest = + new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_UNKNOWN).build(); + + assertThrows( + IllegalStateException.class, + () -> + new Transformer.Builder(context) + .setTransformationRequest(transformationRequest) + .build()); + } + + // TODO(b/209469847): Move this test to TransformationRequestBuilderTest once deprecated + // Transformer.Builder#setOuputMimeType(String) has been removed. + @Test + public void build_withUnsupportedVideoMimeType_throws() { + Context context = ApplicationProvider.getApplicationContext(); + TransformationRequest transformationRequest = + new TransformationRequest.Builder().setVideoMimeType(MimeTypes.VIDEO_UNKNOWN).build(); + + assertThrows( + IllegalStateException.class, + () -> + new Transformer.Builder(context) + .setTransformationRequest(transformationRequest) + .build()); + } } diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java index a72266330c..fdf965012e 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java @@ -224,9 +224,10 @@ public final class TransformerTest { public void startTransformation_flattenForSlowMotion_completesSuccessfully() throws Exception { Transformer transformer = new Transformer.Builder(context) - .setFlattenForSlowMotion(true) .setClock(clock) .setMuxerFactory(new TestMuxerFactory()) + .setTransformationRequest( + new TransformationRequest.Builder().setFlattenForSlowMotion(true).build()) .build(); MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_WITH_SEF_SLOW_MOTION); @@ -243,7 +244,10 @@ public final class TransformerTest { new Transformer.Builder(context) .setClock(clock) .setMuxerFactory(new TestMuxerFactory()) - .setAudioMimeType(MimeTypes.AUDIO_AMR_WB) // unsupported encoder MIME type + .setTransformationRequest( + new TransformationRequest.Builder() + .setAudioMimeType(MimeTypes.AUDIO_AMR_WB) // unsupported encoder MIME type + .build()) .build(); MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_ONLY); @@ -262,7 +266,10 @@ public final class TransformerTest { new Transformer.Builder(context) .setClock(clock) .setMuxerFactory(new TestMuxerFactory()) - .setAudioMimeType(MimeTypes.AUDIO_AAC) // supported encoder MIME type + .setTransformationRequest( + new TransformationRequest.Builder() + .setAudioMimeType(MimeTypes.AUDIO_AAC) // supported encoder MIME type + .build()) .build(); MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_WITH_ALL_SAMPLE_FORMATS_UNSUPPORTED); @@ -281,7 +288,10 @@ public final class TransformerTest { new Transformer.Builder(context) .setClock(clock) .setMuxerFactory(new TestMuxerFactory()) - .setVideoMimeType(MimeTypes.VIDEO_H263) // unsupported encoder MIME type + .setTransformationRequest( + new TransformationRequest.Builder() + .setVideoMimeType(MimeTypes.VIDEO_H263) // unsupported encoder MIME type + .build()) .build(); MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); From ad7c9e25a49e372ad5f467e1fe45c9337e4a1ecd Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 22 Dec 2021 11:58:34 +0000 Subject: [PATCH 40/56] Switch naming convention for shaders Switch to using sentence-case naming convention but with one character prefixes for different types. This is a no-op change. PiperOrigin-RevId: 417791624 --- .../src/main/assets/shaders/fragment_shader.glsl | 6 +++--- .../src/main/assets/shaders/vertex_shader.glsl | 14 +++++++------- .../exoplayer2/transformer/FrameEditor.java | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/library/transformer/src/main/assets/shaders/fragment_shader.glsl b/library/transformer/src/main/assets/shaders/fragment_shader.glsl index 47da1ef8d3..6a53963bb8 100644 --- a/library/transformer/src/main/assets/shaders/fragment_shader.glsl +++ b/library/transformer/src/main/assets/shaders/fragment_shader.glsl @@ -13,8 +13,8 @@ // limitations under the License. #extension GL_OES_EGL_image_external : require precision mediump float; -uniform samplerExternalOES tex_sampler; -varying vec2 v_texcoord; +uniform samplerExternalOES uTexSampler; +varying vec2 vTexCoords; void main() { - gl_FragColor = texture2D(tex_sampler, v_texcoord); + gl_FragColor = texture2D(uTexSampler, vTexCoords); } diff --git a/library/transformer/src/main/assets/shaders/vertex_shader.glsl b/library/transformer/src/main/assets/shaders/vertex_shader.glsl index a7057694c5..3fd3e553fc 100644 --- a/library/transformer/src/main/assets/shaders/vertex_shader.glsl +++ b/library/transformer/src/main/assets/shaders/vertex_shader.glsl @@ -11,12 +11,12 @@ // 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. -attribute vec4 a_position; -attribute vec4 a_texcoord; -uniform mat4 tex_transform; -uniform mat4 transformation_matrix; -varying vec2 v_texcoord; +attribute vec4 aPosition; +attribute vec4 aTexCoords; +uniform mat4 uTexTransform; +uniform mat4 uTransformationMatrix; +varying vec2 vTexCoords; void main() { - gl_Position = transformation_matrix * a_position; - v_texcoord = (tex_transform * a_texcoord).xy; + gl_Position = uTransformationMatrix * aPosition; + vTexCoords = (uTexTransform * aTexCoords).xy; } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java index 6d531237d6..0ff0b95a63 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java @@ -74,7 +74,7 @@ import java.util.concurrent.atomic.AtomicInteger; } glProgram.setBufferAttribute( - "a_position", + "aPosition", new float[] { -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, @@ -83,7 +83,7 @@ import java.util.concurrent.atomic.AtomicInteger; }, /* size= */ 4); glProgram.setBufferAttribute( - "a_texcoord", + "aTexCoords", new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, @@ -91,10 +91,10 @@ import java.util.concurrent.atomic.AtomicInteger; 1.0f, 1.0f, 0.0f, 1.0f, }, /* size= */ 4); - glProgram.setSamplerTexIdUniform("tex_sampler", textureId, /* unit= */ 0); + glProgram.setSamplerTexIdUniform("uTexSampler", textureId, /* unit= */ 0); float[] transformationMatrixArray = getGlMatrixArray(transformationMatrix); - glProgram.setFloatsUniform("transformation_matrix", transformationMatrixArray); + glProgram.setFloatsUniform("uTransformationMatrix", transformationMatrixArray); @Nullable SurfaceView debugSurfaceView = @@ -230,7 +230,7 @@ import java.util.concurrent.atomic.AtomicInteger; public void processData() { inputSurfaceTexture.updateTexImage(); inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); - glProgram.setFloatsUniform("tex_transform", textureTransformMatrix); + glProgram.setFloatsUniform("uTexTransform", textureTransformMatrix); glProgram.bindAttributesAndUniforms(); focusAndDrawQuad(eglSurface, outputWidth, outputHeight); From 4c343ba0fc1a8a18382c9c971a34d835d558b525 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 22 Dec 2021 12:46:46 +0000 Subject: [PATCH 41/56] Add workaround for missing aar tags in POM There is an open Gradle bug that dependencies with AARs are not marked as such in the created POM files (https://github.com/gradle/gradle/issues/3170). This causes issues building ExoPlayer with Maven POMs only. (Issue: google/ExoPlayer#8353). This change adds the workaround suggested on the Gradle bug until the bug is fixed. As we have a mixture of JAR and AAR dependencies, we need to maintain a lookup table to know which dependencies have AARs. The current code throws when a new dependency is added and it's not classified. #minor-release PiperOrigin-RevId: 417797407 --- RELEASENOTES.md | 2 + missing_aar_type_workaround.gradle | 97 ++++++++++++++++++++++++++++++ publish.gradle | 5 ++ 3 files changed, 104 insertions(+) create mode 100644 missing_aar_type_workaround.gradle diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cfde3aa0ac..42041d9020 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -24,6 +24,8 @@ * Amend logic in `AdaptiveTrackSelection` to allow a quality increase under sufficient network bandwidth even if playback is very close to the live edge ((#9784)[https://github.com/google/ExoPlayer/issues/9784]). + * Fix Maven dependency resolution + ((#8353)[https://github.com/google/ExoPlayer/issues/8353]). * Android 12 compatibility: * Upgrade the Cast extension to depend on `com.google.android.gms:play-services-cast-framework:20.1.0`. Earlier diff --git a/missing_aar_type_workaround.gradle b/missing_aar_type_workaround.gradle new file mode 100644 index 0000000000..2bbbd3c2e3 --- /dev/null +++ b/missing_aar_type_workaround.gradle @@ -0,0 +1,97 @@ +// Copyright 2021 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. + +// Workaround for https://github.com/gradle/gradle/issues/3170, adding +// aar in the POM to all dependencies that have AAR files. +def addMissingAarTypeToXml(xml) { + // Dependencies that have JARs only (=don't contain an AAR file). + def jar_only_dependencies = [ + "androidx.annotation:annotation", + "androidx.collection:collection", + "androidx.concurrent:concurrent-futures", + "junit:junit", + "com.google.ads.interactivemedia.v3:interactivemedia", + "com.google.guava:guava", + "com.google.truth:truth", + "com.squareup.okhttp3:okhttp", + "com.squareup.okhttp3:mockwebserver", + "org.mockito:mockito-core", + "org.robolectric:robolectric", + ] + // Dependencies that have AAR files. + def aar_dependencies = [ + "androidx.annotation:annotation-experimental", + "androidx.appcompat:appcompat", + "androidx.core:core", + "androidx.core:core-ktx", + "androidx.leanback:leanback", + "androidx.media:media", + "androidx.media2:media2-session", + "androidx.multidex:multidex", + "androidx.recyclerview:recyclerview", + "androidx.test:core", + "androidx.test.ext:junit", + "androidx.test.ext:truth", + "androidx.work:work-runtime", + "com.google.android.gms:play-services-cast-framework", + "com.google.android.gms:play-services-cronet", + "com.google.android.material:material", + "io.antmedia:rtmp-client", + ] + xml.asNode().children().stream() + .filter { it.name().toString().endsWith("dependencies") } + .forEach { + it.children().stream() + .forEach { + String groupId = + it.children().stream() + .filter { it.name().toString().endsWith("groupId") } + .findFirst() + .get() + .children()[0] + String artifactId = + it.children().stream() + .filter { + it.name().toString().endsWith("artifactId") + } + .findFirst() + .get() + .children()[0] + String dependencyName = groupId + ":" + artifactId + boolean isProjectLibrary = + groupId == 'com.google.android.exoplayer' + boolean hasJar = + jar_only_dependencies.contains(dependencyName) + boolean hasAar = + isProjectLibrary + || aar_dependencies.contains(dependencyName) + if (!hasJar && !hasAar) { + throw new IllegalStateException( + dependencyName + " is not on the JAR or AAR list in missing_aar_type_workaround.gradle") + } + boolean hasTypeDeclaration = + it.children().stream() + .filter { it.name().toString().endsWith("type") } + .findFirst() + .isPresent() + if (hasAar && !hasTypeDeclaration) { + it.appendNode("type", "aar") + } + } + } +} + +ext { + addMissingAarTypeToXml = this.&addMissingAarTypeToXml +} diff --git a/publish.gradle b/publish.gradle index 693a6856a3..44720c32c4 100644 --- a/publish.gradle +++ b/publish.gradle @@ -13,6 +13,8 @@ // limitations under the License. apply plugin: 'maven-publish' +apply from: "$gradle.ext.exoplayerSettingsDir/missing_aar_type_workaround.gradle" + afterEvaluate { publishing { repositories { @@ -46,6 +48,9 @@ afterEvaluate { connection = 'scm:git:https://github.com/google/ExoPlayer.git' url = 'https://github.com/google/ExoPlayer' } + withXml { + addMissingAarTypeToXml(it) + } } } } From 88fe8296335760e68504d52a9e95e8eb817ec622 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 22 Dec 2021 15:34:46 +0000 Subject: [PATCH 42/56] Mark FakeMediaSourceFactory final There's no need to extend this class. Factories for subclasses of FakeMediaSource will need to re-implement createMediaSource, at which point they basically need to re-implement the whole factory interface. PiperOrigin-RevId: 417817499 --- .../android/exoplayer2/testutil/FakeMediaSourceFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactory.java index b740de623e..53a0f7eeba 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactory.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.util.Util; /** Fake {@link MediaSourceFactory} that creates a {@link FakeMediaSource}. */ -public class FakeMediaSourceFactory implements MediaSourceFactory { +public final class FakeMediaSourceFactory implements MediaSourceFactory { /** The window UID used by media sources that are created by the factory. */ public static final Object DEFAULT_WINDOW_UID = new Object(); From 5e8d1eb7f33c037d25c70af7baacd81340ae65e3 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 22 Dec 2021 16:36:42 +0000 Subject: [PATCH 43/56] Add MediaSource.Factory and deprecate MediaSourceFactory This more closely matches the pattern we have for all implementations except DefaultMediaSourceFactory (e.g. ProgressiveMediaSource.Factory) and other factory interfaces like (Http)DataSource.Factory. PiperOrigin-RevId: 417826803 --- .../exoplayer2/demo/PlayerActivity.java | 4 +- docs/ad-insertion.md | 2 +- docs/customization.md | 12 ++--- docs/drm.md | 4 +- docs/media-items.md | 2 +- docs/media-sources.md | 12 ++--- docs/shrinking.md | 10 ++-- docs/transforming-media.md | 2 +- .../exoplayer2/ext/ima/ImaAdsLoader.java | 4 +- .../google/android/exoplayer2/ExoPlayer.java | 33 ++++++------ .../android/exoplayer2/ExoPlayerImpl.java | 7 ++- .../android/exoplayer2/MetadataRetriever.java | 15 +++--- .../android/exoplayer2/SimpleExoPlayer.java | 15 +++--- .../source/DefaultMediaSourceFactory.java | 45 ++++++++-------- .../exoplayer2/source/MediaSource.java | 52 +++++++++++++++++++ .../exoplayer2/source/MediaSourceFactory.java | 44 ++-------------- .../source/ProgressiveMediaSource.java | 1 + .../exoplayer2/source/ads/AdsMediaSource.java | 5 +- .../e2etest/WebvttPlaybackTest.java | 4 +- .../source/ads/AdsMediaSourceTest.java | 4 +- .../source/dash/DashMediaSource.java | 1 + .../exoplayer2/source/hls/HlsMediaSource.java | 1 + .../source/rtsp/RtspMediaSource.java | 1 + .../source/smoothstreaming/SsMediaSource.java | 1 + .../transformer/TransformationRequest.java | 6 +-- .../exoplayer2/transformer/Transformer.java | 13 +++-- .../testutil/FakeMediaSourceFactory.java | 2 + .../testutil/TestExoPlayerBuilder.java | 16 +++--- 28 files changed, 168 insertions(+), 150 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 9a19edf804..d0aa4d58af 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -44,7 +44,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitia import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; -import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.ui.StyledPlayerControlView; @@ -261,7 +261,7 @@ public class PlayerActivity extends AppCompatActivity intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false); RenderersFactory renderersFactory = DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders); - MediaSourceFactory mediaSourceFactory = + MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory) .setAdsLoaderProvider(this::getAdsLoader) .setAdViewProvider(playerView); diff --git a/docs/ad-insertion.md b/docs/ad-insertion.md index 090d0e49d9..142de570a4 100644 --- a/docs/ad-insertion.md +++ b/docs/ad-insertion.md @@ -49,7 +49,7 @@ build and inject a `DefaultMediaSourceFactory` configured with an `AdsLoader.Provider` and an `AdViewProvider` when creating the player: ~~~ -MediaSourceFactory mediaSourceFactory = +MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(context) .setAdsLoaderProvider(adsLoaderProvider) .setAdViewProvider(playerView); diff --git a/docs/customization.md b/docs/customization.md index 6be6d39e77..5ac250ce72 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -14,10 +14,10 @@ Components common to all `ExoPlayer` implementations are: * `MediaSource` instances that define media to be played, load the media, and from which the loaded media can be read. `MediaSource` instances are created - from `MediaItem`s by a `MediaSourceFactory` inside the player. They can also + from `MediaItem`s by a `MediaSource.Factory` inside the player. They can also be passed directly to the player using the [media source based playlist API]. -* A `MediaSourceFactory` that converts `MediaItem`s to `MediaSource`s. The - `MediaSourceFactory` is injected when the player is created. +* A `MediaSource.Factory` that converts `MediaItem`s to `MediaSource`s. The + `MediaSource.Factory` is injected when the player is created. * `Renderer`s that render individual components of the media. `Renderer`s are injected when the player is created. * A `TrackSelector` that selects tracks provided by the `MediaSource` to be @@ -245,9 +245,9 @@ required. Some use cases for custom implementations are: appropriate if you wish to obtain media samples to feed to renderers in a custom way, or if you wish to implement custom `MediaSource` compositing behavior. -* `MediaSourceFactory` – Implementing a custom `MediaSourceFactory` allows - an application to customize the way in which `MediaSource`s are created from - `MediaItem`s. +* `MediaSource.Factory` – Implementing a custom `MediaSource.Factory` + allows an application to customize the way in which `MediaSource`s are created + from `MediaItem`s. * `DataSource` – ExoPlayer’s upstream package already contains a number of `DataSource` implementations for different use cases. You may want to implement you own `DataSource` class to load data in another way, such as over diff --git a/docs/drm.md b/docs/drm.md index 4b64640989..0ab2118e99 100644 --- a/docs/drm.md +++ b/docs/drm.md @@ -85,7 +85,7 @@ building the media item. If an app wants to customise the `DrmSessionManager` used for playback, they can implement a `DrmSessionManagerProvider` and pass this to the -`MediaSourceFactory` which is [used when building the player]. The provider can +`MediaSource.Factory` which is [used when building the player]. The provider can choose whether to instantiate a new manager instance each time or not. To always use the same instance: @@ -93,7 +93,7 @@ use the same instance: DrmSessionManager customDrmSessionManager = new CustomDrmSessionManager(/* ... */); // Pass a drm session manager provider to the media source factory. -MediaSourceFactory mediaSourceFactory = +MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory) .setDrmSessionManagerProvider(mediaItem -> customDrmSessionManager); ~~~ diff --git a/docs/media-items.md b/docs/media-items.md index f1c342c2a5..f57b3b9392 100644 --- a/docs/media-items.md +++ b/docs/media-items.md @@ -4,7 +4,7 @@ title: Media items The [playlist API][] is based on `MediaItem`s, which can be conveniently built using `MediaItem.Builder`. Inside the player, media items are converted into -playable `MediaSource`s by a `MediaSourceFactory`. Without +playable `MediaSource`s by a `MediaSource.Factory`. Without [custom configuration]({{ site.baseurl }}/media-sources.html#customizing-media-source-creation), this conversion is carried out by a `DefaultMediaSourceFactory`, which is capable of building complex media sources corresponding to the properties of the diff --git a/docs/media-sources.md b/docs/media-sources.md index 7cfde4af3b..15435baf74 100644 --- a/docs/media-sources.md +++ b/docs/media-sources.md @@ -6,7 +6,7 @@ redirect_from: In ExoPlayer every piece of media is represented by a `MediaItem`. However internally, the player needs `MediaSource` instances to play the content. The -player creates these from media items using a `MediaSourceFactory`. +player creates these from media items using a `MediaSource.Factory`. By default the player uses a `DefaultMediaSourceFactory`, which can create instances of the following content `MediaSource` implementations: @@ -27,13 +27,13 @@ customization. ## Customizing media source creation ## -When building the player, a `MediaSourceFactory` can be injected. For example, if -an app wants to insert ads and use a `CacheDataSource.Factory` to support +When building the player, a `MediaSource.Factory` can be injected. For example, +if an app wants to insert ads and use a `CacheDataSource.Factory` to support caching, an instance of `DefaultMediaSourceFactory` can be configured to match these requirements and injected during player construction: ~~~ -MediaSourceFactory mediaSourceFactory = +MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(cacheDataSourceFactory) .setAdsLoaderProvider(adsLoaderProvider) .setAdViewProvider(playerView); @@ -47,7 +47,7 @@ The [`DefaultMediaSourceFactory` JavaDoc]({{ site.baseurl }}/doc/reference/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.html) describes the available options in more detail. -It's also possible to inject a custom `MediaSourceFactory` implementation, for +It's also possible to inject a custom `MediaSource.Factory` implementation, for example to support creation of a custom media source type. The factory's `createMediaSource(MediaItem)` will be called to create a media source for each media item that is @@ -57,7 +57,7 @@ media item that is The [`ExoPlayer`] interface defines additional playlist methods that accept media sources rather than media items. This makes it possible to bypass the -player's internal `MediaSourceFactory` and pass media source instances to the +player's internal `MediaSource.Factory` and pass media source instances to the player directly: ~~~ diff --git a/docs/shrinking.md b/docs/shrinking.md index 7c97c9ea29..f8eb4ffbf6 100644 --- a/docs/shrinking.md +++ b/docs/shrinking.md @@ -106,9 +106,9 @@ ExoPlayer player = ## Custom `MediaSource` instantiation ## -If your app is using a custom `MediaSourceFactory` and you want +If your app is using a custom `MediaSource.Factory` and you want `DefaultMediaSourceFactory` to be removed by code stripping, you should pass -your `MediaSourceFactory` directly to the `ExoPlayer.Builder` constructor. +your `MediaSource.Factory` directly to the `ExoPlayer.Builder` constructor. ~~~ ExoPlayer player = @@ -117,13 +117,13 @@ ExoPlayer player = {: .language-java} If your app is using `MediaSource`s directly instead of `MediaItem`s you should -pass `MediaSourceFactory.UNSUPPORTED` to the `ExoPlayer.Builder` constructor, to -ensure `DefaultMediaSourceFactory` and `DefaultExtractorsFactory` can be +pass `MediaSource.Factory.UNSUPPORTED` to the `ExoPlayer.Builder` constructor, +to ensure `DefaultMediaSourceFactory` and `DefaultExtractorsFactory` can be stripped by code shrinking. ~~~ ExoPlayer player = - new ExoPlayer.Builder(context, MediaSourceFactory.UNSUPPORTED).build(); + new ExoPlayer.Builder(context, MediaSource.Factory.UNSUPPORTED).build(); ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory( dataSourceFactory, customExtractorsFactory) diff --git a/docs/transforming-media.md b/docs/transforming-media.md index db0e893839..a3c112f77a 100644 --- a/docs/transforming-media.md +++ b/docs/transforming-media.md @@ -41,7 +41,7 @@ transformer.startTransformation(inputMediaItem, outputPath); ~~~ {: .language-java} -Other parameters, such as the `MediaSourceFactory`, can be passed to the +Other parameters, such as the `MediaSource.Factory`, can be passed to the builder. `startTransformation` receives a `MediaItem` describing the input, and a path or diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index fa1ac96881..f9564154ca 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -46,7 +46,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.ui.AdViewProvider; @@ -222,7 +222,7 @@ public final class ImaAdsLoader implements AdsLoader { /** * Sets the MIME types to prioritize for linear ad media. If not specified, MIME types supported - * by the {@link MediaSourceFactory adMediaSourceFactory} used to construct the {@link + * by the {@link MediaSource.Factory adMediaSourceFactory} used to construct the {@link * AdsMediaSource} will be used. * * @param adMediaMimeTypes The MIME types to prioritize for linear ad media. May contain {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index e6ff11dfd9..53b196fbb9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -43,7 +43,6 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; @@ -77,7 +76,7 @@ import java.util.List; *
            *
          • {@link MediaSource MediaSources} that define the media to be played, load the media, * and from which the loaded media can be read. MediaSources are created from {@link MediaItem - * MediaItems} by the {@link MediaSourceFactory} injected into the player {@link + * MediaItems} by the {@link MediaSource.Factory} injected into the player {@link * Builder#setMediaSourceFactory Builder}, or can be added directly by methods like {@link * #setMediaSource(MediaSource)}. The library provides a {@link DefaultMediaSourceFactory} for * progressive media files, DASH, SmoothStreaming and HLS, which also includes functionality @@ -368,7 +367,7 @@ public interface ExoPlayer extends Player { /* package */ Clock clock; /* package */ long foregroundModeTimeoutMs; /* package */ Supplier renderersFactorySupplier; - /* package */ Supplier mediaSourceFactorySupplier; + /* package */ Supplier mediaSourceFactorySupplier; /* package */ Supplier trackSelectorSupplier; /* package */ Supplier loadControlSupplier; /* package */ Supplier bandwidthMeterSupplier; @@ -396,7 +395,7 @@ public interface ExoPlayer extends Player { * Creates a builder. * *

            Use {@link #Builder(Context, RenderersFactory)}, {@link #Builder(Context, - * MediaSourceFactory)} or {@link #Builder(Context, RenderersFactory, MediaSourceFactory)} + * MediaSource.Factory)} or {@link #Builder(Context, RenderersFactory, MediaSource.Factory)} * instead, if you intend to provide a custom {@link RenderersFactory}, {@link * ExtractorsFactory} or {@link DefaultMediaSourceFactory}. This is to ensure that ProGuard or * R8 can remove ExoPlayer's {@link DefaultRenderersFactory}, {@link DefaultExtractorsFactory} @@ -407,7 +406,7 @@ public interface ExoPlayer extends Player { *

              *
            • {@link RenderersFactory}: {@link DefaultRenderersFactory} *
            • {@link TrackSelector}: {@link DefaultTrackSelector} - *
            • {@link MediaSourceFactory}: {@link DefaultMediaSourceFactory} + *
            • {@link MediaSource.Factory}: {@link DefaultMediaSourceFactory} *
            • {@link LoadControl}: {@link DefaultLoadControl} *
            • {@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)} *
            • {@link LivePlaybackSpeedControl}: {@link DefaultLivePlaybackSpeedControl} @@ -462,7 +461,7 @@ public interface ExoPlayer extends Player { } /** - * Creates a builder with a custom {@link MediaSourceFactory}. + * Creates a builder with a custom {@link MediaSource.Factory}. * *

              See {@link #Builder(Context)} for a list of default values. * @@ -474,12 +473,12 @@ public interface ExoPlayer extends Player { * @param mediaSourceFactory A factory for creating a {@link MediaSource} from a {@link * MediaItem}. */ - public Builder(Context context, MediaSourceFactory mediaSourceFactory) { + public Builder(Context context, MediaSource.Factory mediaSourceFactory) { this(context, () -> new DefaultRenderersFactory(context), () -> mediaSourceFactory); } /** - * Creates a builder with a custom {@link RenderersFactory} and {@link MediaSourceFactory}. + * Creates a builder with a custom {@link RenderersFactory} and {@link MediaSource.Factory}. * *

              See {@link #Builder(Context)} for a list of default values. * @@ -494,7 +493,9 @@ public interface ExoPlayer extends Player { * MediaItem}. */ public Builder( - Context context, RenderersFactory renderersFactory, MediaSourceFactory mediaSourceFactory) { + Context context, + RenderersFactory renderersFactory, + MediaSource.Factory mediaSourceFactory) { this(context, () -> renderersFactory, () -> mediaSourceFactory); } @@ -507,7 +508,7 @@ public interface ExoPlayer extends Player { * @param context A {@link Context}. * @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the * player. - * @param mediaSourceFactory A {@link MediaSourceFactory}. + * @param mediaSourceFactory A {@link MediaSource.Factory}. * @param trackSelector A {@link TrackSelector}. * @param loadControl A {@link LoadControl}. * @param bandwidthMeter A {@link BandwidthMeter}. @@ -516,7 +517,7 @@ public interface ExoPlayer extends Player { public Builder( Context context, RenderersFactory renderersFactory, - MediaSourceFactory mediaSourceFactory, + MediaSource.Factory mediaSourceFactory, TrackSelector trackSelector, LoadControl loadControl, BandwidthMeter bandwidthMeter, @@ -534,7 +535,7 @@ public interface ExoPlayer extends Player { private Builder( Context context, Supplier renderersFactorySupplier, - Supplier mediaSourceFactorySupplier) { + Supplier mediaSourceFactorySupplier) { this( context, renderersFactorySupplier, @@ -548,7 +549,7 @@ public interface ExoPlayer extends Player { private Builder( Context context, Supplier renderersFactorySupplier, - Supplier mediaSourceFactorySupplier, + Supplier mediaSourceFactorySupplier, Supplier trackSelectorSupplier, Supplier loadControlSupplier, Supplier bandwidthMeterSupplier, @@ -607,13 +608,13 @@ public interface ExoPlayer extends Player { } /** - * Sets the {@link MediaSourceFactory} that will be used by the player. + * Sets the {@link MediaSource.Factory} that will be used by the player. * - * @param mediaSourceFactory A {@link MediaSourceFactory}. + * @param mediaSourceFactory A {@link MediaSource.Factory}. * @return This builder. * @throws IllegalStateException If {@link #build()} has already been called. */ - public Builder setMediaSourceFactory(MediaSourceFactory mediaSourceFactory) { + public Builder setMediaSourceFactory(MediaSource.Factory mediaSourceFactory) { checkState(!buildCalled); this.mediaSourceFactorySupplier = () -> mediaSourceFactory; return this; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 72e6ce60f8..bbd03b59d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -80,7 +80,6 @@ import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -133,7 +132,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private final Timeline.Window window; private final List mediaSourceHolderSnapshots; private final boolean useLazyPreparation; - private final MediaSourceFactory mediaSourceFactory; + private final MediaSource.Factory mediaSourceFactory; private final AnalyticsCollector analyticsCollector; private final Looper applicationLooper; private final BandwidthMeter bandwidthMeter; @@ -172,7 +171,7 @@ import java.util.concurrent.CopyOnWriteArraySet; * * @param renderers The {@link Renderer}s. * @param trackSelector The {@link TrackSelector}. - * @param mediaSourceFactory The {@link MediaSourceFactory}. + * @param mediaSourceFactory The {@link MediaSource.Factory}. * @param loadControl The {@link LoadControl}. * @param bandwidthMeter The {@link BandwidthMeter}. * @param analyticsCollector The {@link AnalyticsCollector}. @@ -196,7 +195,7 @@ import java.util.concurrent.CopyOnWriteArraySet; public ExoPlayerImpl( Renderer[] renderers, TrackSelector trackSelector, - MediaSourceFactory mediaSourceFactory, + MediaSource.Factory mediaSourceFactory, LoadControl loadControl, BandwidthMeter bandwidthMeter, AnalyticsCollector analyticsCollector, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java b/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java index a3e8081c14..e580fc4ff8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java @@ -30,7 +30,6 @@ import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; @@ -49,7 +48,7 @@ public final class MetadataRetriever { /** * Retrieves the {@link TrackGroupArray} corresponding to a {@link MediaItem}. * - *

              This is equivalent to using {@link #retrieveMetadata(MediaSourceFactory, MediaItem)} with a + *

              This is equivalent to using {@link #retrieveMetadata(MediaSource.Factory, MediaItem)} with a * {@link DefaultMediaSourceFactory} and a {@link DefaultExtractorsFactory} with {@link * Mp4Extractor#FLAG_READ_MOTION_PHOTO_METADATA} and {@link Mp4Extractor#FLAG_READ_SEF_DATA} set. * @@ -67,13 +66,13 @@ public final class MetadataRetriever { * *

              This method is thread-safe. * - * @param mediaSourceFactory mediaSourceFactory The {@link MediaSourceFactory} to use to read the + * @param mediaSourceFactory mediaSourceFactory The {@link MediaSource.Factory} to use to read the * data. * @param mediaItem The {@link MediaItem} whose metadata should be retrieved. * @return A {@link ListenableFuture} of the result. */ public static ListenableFuture retrieveMetadata( - MediaSourceFactory mediaSourceFactory, MediaItem mediaItem) { + MediaSource.Factory mediaSourceFactory, MediaItem mediaItem) { return retrieveMetadata(mediaSourceFactory, mediaItem, Clock.DEFAULT); } @@ -84,13 +83,13 @@ public final class MetadataRetriever { new DefaultExtractorsFactory() .setMp4ExtractorFlags( Mp4Extractor.FLAG_READ_MOTION_PHOTO_METADATA | Mp4Extractor.FLAG_READ_SEF_DATA); - MediaSourceFactory mediaSourceFactory = + MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(context, extractorsFactory); return retrieveMetadata(mediaSourceFactory, mediaItem, clock); } private static ListenableFuture retrieveMetadata( - MediaSourceFactory mediaSourceFactory, MediaItem mediaItem, Clock clock) { + MediaSource.Factory mediaSourceFactory, MediaItem mediaItem, Clock clock) { // Recreate thread and handler every time this method is called so that it can be used // concurrently. return new MetadataRetrieverInternal(mediaSourceFactory, clock).retrieveMetadata(mediaItem); @@ -103,12 +102,12 @@ public final class MetadataRetriever { private static final int MESSAGE_CONTINUE_LOADING = 2; private static final int MESSAGE_RELEASE = 3; - private final MediaSourceFactory mediaSourceFactory; + private final MediaSource.Factory mediaSourceFactory; private final HandlerThread mediaSourceThread; private final HandlerWrapper mediaSourceHandler; private final SettableFuture trackGroupsFuture; - public MetadataRetrieverInternal(MediaSourceFactory mediaSourceFactory, Clock clock) { + public MetadataRetrieverInternal(MediaSource.Factory mediaSourceFactory, Clock clock) { this.mediaSourceFactory = mediaSourceFactory; mediaSourceThread = new HandlerThread("ExoPlayer:MetadataRetriever"); mediaSourceThread.start(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 6c4b6cc45c..519904bf2e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -58,7 +58,6 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; @@ -113,7 +112,7 @@ public class SimpleExoPlayer extends BasePlayer } /** - * @deprecated Use {@link ExoPlayer.Builder#Builder(Context, MediaSourceFactory)} and {@link + * @deprecated Use {@link ExoPlayer.Builder#Builder(Context, MediaSource.Factory)} and {@link * DefaultMediaSourceFactory#DefaultMediaSourceFactory(Context, ExtractorsFactory)} instead. */ @Deprecated @@ -124,7 +123,7 @@ public class SimpleExoPlayer extends BasePlayer /** * @deprecated Use {@link ExoPlayer.Builder#Builder(Context, RenderersFactory, - * MediaSourceFactory)} and {@link + * MediaSource.Factory)} and {@link * DefaultMediaSourceFactory#DefaultMediaSourceFactory(Context, ExtractorsFactory)} instead. */ @Deprecated @@ -137,7 +136,7 @@ public class SimpleExoPlayer extends BasePlayer /** * @deprecated Use {@link ExoPlayer.Builder#Builder(Context, RenderersFactory, - * MediaSourceFactory, TrackSelector, LoadControl, BandwidthMeter, AnalyticsCollector)} + * MediaSource.Factory, TrackSelector, LoadControl, BandwidthMeter, AnalyticsCollector)} * instead. */ @Deprecated @@ -145,7 +144,7 @@ public class SimpleExoPlayer extends BasePlayer Context context, RenderersFactory renderersFactory, TrackSelector trackSelector, - MediaSourceFactory mediaSourceFactory, + MediaSource.Factory mediaSourceFactory, LoadControl loadControl, BandwidthMeter bandwidthMeter, AnalyticsCollector analyticsCollector) { @@ -178,10 +177,10 @@ public class SimpleExoPlayer extends BasePlayer } /** - * @deprecated Use {@link ExoPlayer.Builder#setMediaSourceFactory(MediaSourceFactory)} instead. + * @deprecated Use {@link ExoPlayer.Builder#setMediaSourceFactory(MediaSource.Factory)} instead. */ @Deprecated - public Builder setMediaSourceFactory(MediaSourceFactory mediaSourceFactory) { + public Builder setMediaSourceFactory(MediaSource.Factory mediaSourceFactory) { wrappedBuilder.setMediaSourceFactory(mediaSourceFactory); return this; } @@ -400,7 +399,7 @@ public class SimpleExoPlayer extends BasePlayer Context context, RenderersFactory renderersFactory, TrackSelector trackSelector, - MediaSourceFactory mediaSourceFactory, + MediaSource.Factory mediaSourceFactory, LoadControl loadControl, BandwidthMeter bandwidthMeter, AnalyticsCollector analyticsCollector, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java index 222d67c1ca..1b421de43b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java @@ -58,7 +58,7 @@ import java.util.Set; import org.checkerframework.checker.nullness.compatqual.NullableType; /** - * The default {@link MediaSourceFactory} implementation. + * The default {@link MediaSource.Factory} implementation. * *

              This implementation delegates calls to {@link #createMediaSource(MediaItem)} to the following * factories: @@ -92,6 +92,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * configuration}, {@link #setAdsLoaderProvider} and {@link #setAdViewProvider} need to be called to * configure the factory with the required providers. */ +@SuppressWarnings("deprecation") // Implement deprecated type for backwards compatibility. public final class DefaultMediaSourceFactory implements MediaSourceFactory { /** @deprecated Use {@link AdsLoader.Provider} instead. */ @@ -103,7 +104,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { private final DataSource.Factory dataSourceFactory; private final DelegateFactoryLoader delegateFactoryLoader; - @Nullable private final MediaSourceFactory serverSideDaiMediaSourceFactory; + @Nullable private final MediaSource.Factory serverSideDaiMediaSourceFactory; @Nullable private AdsLoader.Provider adsLoaderProvider; @Nullable private AdViewProvider adViewProvider; @Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy; @@ -157,13 +158,13 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { * for requesting media data. * @param extractorsFactory An {@link ExtractorsFactory} used to extract progressive media from * its container. - * @param serverSideDaiMediaSourceFactory A {@link MediaSourceFactory} for creating server side + * @param serverSideDaiMediaSourceFactory A {@link MediaSource.Factory} for creating server side * inserted ad media sources. */ public DefaultMediaSourceFactory( DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, - @Nullable MediaSourceFactory serverSideDaiMediaSourceFactory) { + @Nullable MediaSource.Factory serverSideDaiMediaSourceFactory) { this.dataSourceFactory = dataSourceFactory; // Temporary until factory registration is agreed upon. this.serverSideDaiMediaSourceFactory = serverSideDaiMediaSourceFactory; @@ -309,7 +310,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { Util.inferContentTypeForUriAndMimeType( mediaItem.localConfiguration.uri, mediaItem.localConfiguration.mimeType); @Nullable - MediaSourceFactory mediaSourceFactory = delegateFactoryLoader.getMediaSourceFactory(type); + MediaSource.Factory mediaSourceFactory = delegateFactoryLoader.getMediaSourceFactory(type); checkStateNotNull( mediaSourceFactory, "No suitable media source factory found for content type: " + type); @@ -435,10 +436,10 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { private static final class DelegateFactoryLoader { private final DataSource.Factory dataSourceFactory; private final ExtractorsFactory extractorsFactory; - private final Map> + private final Map> mediaSourceFactorySuppliers; private final Set supportedTypes; - private final Map mediaSourceFactories; + private final Map mediaSourceFactories; @Nullable private DrmSessionManagerProvider drmSessionManagerProvider; @Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy; @@ -460,13 +461,13 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { @SuppressWarnings("deprecation") // Forwarding to deprecated methods. @Nullable - public MediaSourceFactory getMediaSourceFactory(@C.ContentType int contentType) { - @Nullable MediaSourceFactory mediaSourceFactory = mediaSourceFactories.get(contentType); + public MediaSource.Factory getMediaSourceFactory(@C.ContentType int contentType) { + @Nullable MediaSource.Factory mediaSourceFactory = mediaSourceFactories.get(contentType); if (mediaSourceFactory != null) { return mediaSourceFactory; } @Nullable - Supplier mediaSourceFactorySupplier = maybeLoadSupplier(contentType); + Supplier mediaSourceFactorySupplier = maybeLoadSupplier(contentType); if (mediaSourceFactorySupplier == null) { return null; } @@ -485,7 +486,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { public void setDrmSessionManagerProvider( @Nullable DrmSessionManagerProvider drmSessionManagerProvider) { this.drmSessionManagerProvider = drmSessionManagerProvider; - for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) { + for (MediaSource.Factory mediaSourceFactory : mediaSourceFactories.values()) { mediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider); } } @@ -493,7 +494,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { public void setLoadErrorHandlingPolicy( @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; - for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) { + for (MediaSource.Factory mediaSourceFactory : mediaSourceFactories.values()) { mediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy); } } @@ -507,38 +508,38 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { } @Nullable - private Supplier maybeLoadSupplier(@C.ContentType int contentType) { + private Supplier maybeLoadSupplier(@C.ContentType int contentType) { if (mediaSourceFactorySuppliers.containsKey(contentType)) { return mediaSourceFactorySuppliers.get(contentType); } - @Nullable Supplier mediaSourceFactorySupplier = null; + @Nullable Supplier mediaSourceFactorySupplier = null; try { - Class clazz; + Class clazz; switch (contentType) { case C.TYPE_DASH: clazz = Class.forName("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory") - .asSubclass(MediaSourceFactory.class); + .asSubclass(MediaSource.Factory.class); mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory); break; case C.TYPE_SS: clazz = Class.forName( "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory") - .asSubclass(MediaSourceFactory.class); + .asSubclass(MediaSource.Factory.class); mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory); break; case C.TYPE_HLS: clazz = Class.forName("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory") - .asSubclass(MediaSourceFactory.class); + .asSubclass(MediaSource.Factory.class); mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory); break; case C.TYPE_RTSP: clazz = Class.forName("com.google.android.exoplayer2.source.rtsp.RtspMediaSource$Factory") - .asSubclass(MediaSourceFactory.class); + .asSubclass(MediaSource.Factory.class); mediaSourceFactorySupplier = () -> newInstance(clazz); break; case C.TYPE_OTHER: @@ -600,8 +601,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { public void release() {} } - private static MediaSourceFactory newInstance( - Class clazz, DataSource.Factory dataSourceFactory) { + private static MediaSource.Factory newInstance( + Class clazz, DataSource.Factory dataSourceFactory) { try { return clazz.getConstructor(DataSource.Factory.class).newInstance(dataSourceFactory); } catch (Exception e) { @@ -609,7 +610,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { } } - private static MediaSourceFactory newInstance(Class clazz) { + private static MediaSource.Factory newInstance(Class clazz) { try { return clazz.getConstructor().newInstance(); } catch (Exception e) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index fc9f38a530..1b37454953 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -17,12 +17,18 @@ package com.google.android.exoplayer2.source; import android.os.Handler; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.PlayerId; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider; import com.google.android.exoplayer2.drm.DrmSessionEventListener; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManagerProvider; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; @@ -47,6 +53,52 @@ import java.io.IOException; */ public interface MediaSource { + /** Factory for creating {@link MediaSource MediaSources} from {@link MediaItem MediaItems}. */ + interface Factory { + + /** + * An instance that throws {@link UnsupportedOperationException} from {@link #createMediaSource} + * and {@link #getSupportedTypes()}. + */ + @SuppressWarnings("deprecation") + Factory UNSUPPORTED = MediaSourceFactory.UNSUPPORTED; + + /** + * Sets the {@link DrmSessionManagerProvider} used to obtain a {@link DrmSessionManager} for a + * {@link MediaItem}. + * + *

              If not set, {@link DefaultDrmSessionManagerProvider} is used. + * + * @return This factory, for convenience. + */ + Factory setDrmSessionManagerProvider( + @Nullable DrmSessionManagerProvider drmSessionManagerProvider); + + /** + * Sets an optional {@link LoadErrorHandlingPolicy}. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}, or {@code null} to use the + * {@link DefaultLoadErrorHandlingPolicy}. + * @return This factory, for convenience. + */ + Factory setLoadErrorHandlingPolicy(@Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy); + + /** + * Returns the {@link C.ContentType content types} supported by media sources created by this + * factory. + */ + @C.ContentType + int[] getSupportedTypes(); + + /** + * Creates a new {@link MediaSource} with the specified {@link MediaItem}. + * + * @param mediaItem The media item to play. + * @return The new {@link MediaSource media source}. + */ + MediaSource createMediaSource(MediaItem mediaItem); + } + /** A caller of media sources, which will be notified of source events. */ interface MediaSourceCaller { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java index 54d9eb2658..04e7712608 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java @@ -18,14 +18,12 @@ package com.google.android.exoplayer2.source; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider; -import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManagerProvider; -import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; -/** Factory for creating {@link MediaSource MediaSources} from {@link MediaItem MediaItems}. */ -public interface MediaSourceFactory { +/** @deprecated Use {@link MediaSource.Factory}. */ +@Deprecated +public interface MediaSourceFactory extends MediaSource.Factory { /** * An instance that throws {@link UnsupportedOperationException} from {@link #createMediaSource} @@ -56,40 +54,4 @@ public interface MediaSourceFactory { throw new UnsupportedOperationException(); } }; - - /** - * Sets the {@link DrmSessionManagerProvider} used to obtain a {@link DrmSessionManager} for a - * {@link MediaItem}. - * - *

              If not set, {@link DefaultDrmSessionManagerProvider} is used. - * - * @return This factory, for convenience. - */ - MediaSourceFactory setDrmSessionManagerProvider( - @Nullable DrmSessionManagerProvider drmSessionManagerProvider); - - /** - * Sets an optional {@link LoadErrorHandlingPolicy}. - * - * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}, or {@code null} to use the - * {@link DefaultLoadErrorHandlingPolicy}. - * @return This factory, for convenience. - */ - MediaSourceFactory setLoadErrorHandlingPolicy( - @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy); - - /** - * Returns the {@link C.ContentType content types} supported by media sources created by this - * factory. - */ - @C.ContentType - int[] getSupportedTypes(); - - /** - * Creates a new {@link MediaSource} with the specified {@link MediaItem}. - * - * @param mediaItem The media item to play. - * @return The new {@link MediaSource media source}. - */ - MediaSource createMediaSource(MediaItem mediaItem); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index 53b2c90108..cf17a0bdb4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -50,6 +50,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource implements ProgressiveMediaPeriod.Listener { /** Factory for {@link ProgressiveMediaSource}s. */ + @SuppressWarnings("deprecation") // Implement deprecated type for backwards compatibility. public static final class Factory implements MediaSourceFactory { private final DataSource.Factory dataSourceFactory; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 09208060a1..4667cbea05 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener; -import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.ui.AdViewProvider; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSpec; @@ -127,7 +126,7 @@ public final class AdsMediaSource extends CompositeMediaSource { new MediaPeriodId(/* periodUid= */ new Object()); private final MediaSource contentMediaSource; - private final MediaSourceFactory adMediaSourceFactory; + private final MediaSource.Factory adMediaSourceFactory; private final AdsLoader adsLoader; private final AdViewProvider adViewProvider; private final DataSpec adTagDataSpec; @@ -159,7 +158,7 @@ public final class AdsMediaSource extends CompositeMediaSource { MediaSource contentMediaSource, DataSpec adTagDataSpec, Object adsId, - MediaSourceFactory adMediaSourceFactory, + MediaSource.Factory adMediaSourceFactory, AdsLoader adsLoader, AdViewProvider adViewProvider) { this.contentMediaSource = contentMediaSource; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/WebvttPlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/WebvttPlaybackTest.java index 49b29f3c5c..882eea70d1 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/WebvttPlaybackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/WebvttPlaybackTest.java @@ -28,7 +28,7 @@ import com.google.android.exoplayer2.robolectric.PlaybackOutput; import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig; import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; -import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.CapturingRenderersFactory; import com.google.android.exoplayer2.testutil.DumpFileAsserts; import com.google.android.exoplayer2.testutil.FakeClock; @@ -58,7 +58,7 @@ public class WebvttPlaybackTest { Context applicationContext = ApplicationProvider.getApplicationContext(); CapturingRenderersFactory capturingRenderersFactory = new CapturingRenderersFactory(applicationContext); - MediaSourceFactory mediaSourceFactory = + MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(applicationContext) .experimentalUseProgressiveMediaSourceForSubtitles(true); ExoPlayer player = diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java index 5e69dfc755..e5f5f2b8df 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java @@ -31,9 +31,9 @@ import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; -import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.ads.AdsLoader.EventListener; import com.google.android.exoplayer2.testutil.FakeMediaSource; @@ -101,7 +101,7 @@ public final class AdsMediaSourceTest { // later. contentMediaSource = new FakeMediaSource(/* timeline= */ null); prerollAdMediaSource = new FakeMediaSource(/* timeline= */ null); - MediaSourceFactory adMediaSourceFactory = mock(MediaSourceFactory.class); + MediaSource.Factory adMediaSourceFactory = mock(MediaSource.Factory.class); when(adMediaSourceFactory.createMediaSource(any(MediaItem.class))) .thenReturn(prerollAdMediaSource); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 7b6df6cbf2..32b8eac0e7 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -97,6 +97,7 @@ public final class DashMediaSource extends BaseMediaSource { } /** Factory for {@link DashMediaSource}s. */ + @SuppressWarnings("deprecation") // Implement deprecated type for backwards compatibility. public static final class Factory implements MediaSourceFactory { private final DashChunkSource.Factory chunkSourceFactory; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 43e9645dc5..70fbb52c61 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -92,6 +92,7 @@ public final class HlsMediaSource extends BaseMediaSource public static final int METADATA_TYPE_EMSG = 3; /** Factory for {@link HlsMediaSource}s. */ + @SuppressWarnings("deprecation") // Implement deprecated type for backwards compatibility. public static final class Factory implements MediaSourceFactory { private final HlsDataSourceFactory hlsDataSourceFactory; diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java index ddfb70651c..5fd1e6cb04 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java @@ -61,6 +61,7 @@ public final class RtspMediaSource extends BaseMediaSource { *

            • {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} *
            */ + @SuppressWarnings("deprecation") // Implement deprecated type for backwards compatibility. public static final class Factory implements MediaSourceFactory { private long timeoutMs; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 503a84f2ae..977bac524a 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -76,6 +76,7 @@ public final class SsMediaSource extends BaseMediaSource } /** Factory for {@link SsMediaSource}. */ + @SuppressWarnings("deprecation") // Implement deprecated type for backwards compatibility. public static final class Factory implements MediaSourceFactory { private final SsChunkSource.Factory chunkSourceFactory; diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationRequest.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationRequest.java index 823eaf3fb1..ce614235b5 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationRequest.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationRequest.java @@ -20,7 +20,7 @@ import android.graphics.Matrix; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; -import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -92,8 +92,8 @@ public final class TransformationRequest { *
          • The recording frame rate of the video is 120 or 240 fps. *
          * - *

          If specifying a {@link MediaSourceFactory} using {@link - * Transformer.Builder#setMediaSourceFactory(MediaSourceFactory)}, make sure that {@link + *

          If specifying a {@link MediaSource.Factory} using {@link + * Transformer.Builder#setMediaSourceFactory(MediaSource.Factory)}, make sure that {@link * Mp4Extractor#FLAG_READ_SEF_DATA} is set on the {@link Mp4Extractor} used. Otherwise, the slow * motion metadata will be ignored and the input won't be flattened. * diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java index 636a137d52..fc4a96957b 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java @@ -51,7 +51,6 @@ import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.util.Clock; @@ -93,7 +92,7 @@ public final class Transformer { private @MonotonicNonNull Context context; // Optional fields. - private @MonotonicNonNull MediaSourceFactory mediaSourceFactory; + private MediaSource.@MonotonicNonNull Factory mediaSourceFactory; private Muxer.Factory muxerFactory; private boolean removeAudio; private boolean removeVideo; @@ -171,14 +170,14 @@ public final class Transformer { } /** - * Sets the {@link MediaSourceFactory} to be used to retrieve the inputs to transform. The + * Sets the {@link MediaSource.Factory} to be used to retrieve the inputs to transform. The * default value is a {@link DefaultMediaSourceFactory} built with the context provided in * {@link #Builder(Context) the constructor}. * - * @param mediaSourceFactory A {@link MediaSourceFactory}. + * @param mediaSourceFactory A {@link MediaSource.Factory}. * @return This builder. */ - public Builder setMediaSourceFactory(MediaSourceFactory mediaSourceFactory) { + public Builder setMediaSourceFactory(MediaSource.Factory mediaSourceFactory) { this.mediaSourceFactory = mediaSourceFactory; return this; } @@ -471,7 +470,7 @@ public final class Transformer { public static final int PROGRESS_STATE_NO_TRANSFORMATION = 4; private final Context context; - private final MediaSourceFactory mediaSourceFactory; + private final MediaSource.Factory mediaSourceFactory; private final Muxer.Factory muxerFactory; private final boolean removeAudio; private final boolean removeVideo; @@ -490,7 +489,7 @@ public final class Transformer { private Transformer( Context context, - MediaSourceFactory mediaSourceFactory, + MediaSource.Factory mediaSourceFactory, Muxer.Factory muxerFactory, boolean removeAudio, boolean removeVideo, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactory.java index 53a0f7eeba..42b2da9fd1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactory.java @@ -27,6 +27,8 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.util.Util; /** Fake {@link MediaSourceFactory} that creates a {@link FakeMediaSource}. */ +// Implement and return deprecated type for backwards compatibility. +@SuppressWarnings("deprecation") public final class FakeMediaSourceFactory implements MediaSourceFactory { /** The window UID used by media sources that are created by the factory. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayerBuilder.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayerBuilder.java index 9a668f0186..b607c52723 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayerBuilder.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayerBuilder.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.analytics.AnalyticsCollector; -import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; @@ -46,7 +46,7 @@ public class TestExoPlayerBuilder { private BandwidthMeter bandwidthMeter; @Nullable private Renderer[] renderers; @Nullable private RenderersFactory renderersFactory; - @Nullable private MediaSourceFactory mediaSourceFactory; + @Nullable private MediaSource.Factory mediaSourceFactory; private boolean useLazyPreparation; private @MonotonicNonNull Looper looper; private long seekBackIncrementMs; @@ -222,21 +222,21 @@ public class TestExoPlayerBuilder { } /** - * Returns the {@link MediaSourceFactory} that will be used by the player, or null if no {@link - * MediaSourceFactory} has been set yet and no default is available. + * Returns the {@link MediaSource.Factory} that will be used by the player, or null if no {@link + * MediaSource.Factory} has been set yet and no default is available. */ @Nullable - public MediaSourceFactory getMediaSourceFactory() { + public MediaSource.Factory getMediaSourceFactory() { return mediaSourceFactory; } /** - * Sets the {@link MediaSourceFactory} to be used by the player. + * Sets the {@link MediaSource.Factory} to be used by the player. * - * @param mediaSourceFactory The {@link MediaSourceFactory} to be used by the player. + * @param mediaSourceFactory The {@link MediaSource.Factory} to be used by the player. * @return This builder. */ - public TestExoPlayerBuilder setMediaSourceFactory(MediaSourceFactory mediaSourceFactory) { + public TestExoPlayerBuilder setMediaSourceFactory(MediaSource.Factory mediaSourceFactory) { this.mediaSourceFactory = mediaSourceFactory; return this; } From 086688ed3c7fdd2784dcbd62d6c407ba401ff9dd Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 23 Dec 2021 09:08:04 +0000 Subject: [PATCH 44/56] Fix reference to deprecated ExoPlayer.retry() method in dev guide. #minor-release PiperOrigin-RevId: 417959956 --- docs/listening-to-player-events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/listening-to-player-events.md b/docs/listening-to-player-events.md index 3de8bde96b..5bf7f7b53d 100644 --- a/docs/listening-to-player-events.md +++ b/docs/listening-to-player-events.md @@ -71,7 +71,7 @@ Errors that cause playback to fail can be received by implementing `onPlayerError(PlaybackException error)` in a registered `Player.Listener`. When a failure occurs, this method will be called immediately before the playback state transitions to `Player.STATE_IDLE`. -Failed or stopped playbacks can be retried by calling `ExoPlayer.retry`. +Failed or stopped playbacks can be retried by calling `ExoPlayer.prepare`. Note that some [`Player`][] implementations pass instances of subclasses of `PlaybackException` to provide additional information about the failure. For From fa419f21a9a109b9467c77d4ce1eab23b82cfb91 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 23 Dec 2021 09:22:57 +0000 Subject: [PATCH 45/56] Fix deprecated reference to Player.EventListener in the dev guide #minor-release PiperOrigin-RevId: 417961565 --- docs/retrieving-metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/retrieving-metadata.md b/docs/retrieving-metadata.md index 1b0339d8af..b0a288353a 100644 --- a/docs/retrieving-metadata.md +++ b/docs/retrieving-metadata.md @@ -6,7 +6,7 @@ title: Retrieving metadata The metadata of the media can be retrieved during playback in multiple ways. The most straightforward is to listen for the -`Player.EventListener#onMediaMetadataChanged` event; this will provide a +`Player.Listener#onMediaMetadataChanged` event; this will provide a [`MediaMetadata`][] object for use, which has fields such as `title` and `albumArtist`. Alternatively, calling `Player#getMediaMetadata` returns the same object. From 624338ccbd59244bcd9d9c396445da5261716425 Mon Sep 17 00:00:00 2001 From: huangdarwin Date: Thu, 23 Dec 2021 12:10:07 +0000 Subject: [PATCH 46/56] Transformer GL: Document lack of support for non-square pixels. This may one day change, but at least for now, we don't intend to support non-square pixels. PiperOrigin-RevId: 417983516 --- .../exoplayer2/transformer/FrameEditorTest.java | 3 +++ .../exoplayer2/transformer/FrameEditor.java | 16 +++++++++++++++- .../transformer/TransformationException.java | 2 +- .../transformer/VideoSamplePipeline.java | 1 + 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameEditorTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameEditorTest.java index 692633c192..b47205947f 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameEditorTest.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameEditorTest.java @@ -60,6 +60,8 @@ public final class FrameEditorTest { private static final int DEQUEUE_TIMEOUT_US = 5_000_000; /** Time to wait for the frame editor's input to be populated by the decoder, in milliseconds. */ private static final int SURFACE_WAIT_MS = 1000; + /** The ratio of width over height, for each pixel in a frame. */ + private static final float PIXEL_WIDTH_HEIGHT_RATIO = 1; private @MonotonicNonNull FrameEditor frameEditor; private @MonotonicNonNull ImageReader frameEditorOutputImageReader; @@ -91,6 +93,7 @@ public final class FrameEditorTest { getApplicationContext(), width, height, + PIXEL_WIDTH_HEIGHT_RATIO, identityMatrix, frameEditorOutputImageReader.getSurface(), Transformer.DebugViewProvider.NONE); diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java index 0ff0b95a63..1846cd956e 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java @@ -47,6 +47,7 @@ import java.util.concurrent.atomic.AtomicInteger; * @param context A {@link Context}. * @param outputWidth The output width in pixels. * @param outputHeight The output height in pixels. + * @param pixelWidthHeightRatio The ratio of width over height, for each pixel. * @param transformationMatrix The transformation matrix to apply to each frame. * @param outputSurface The {@link Surface}. * @param debugViewProvider Provider for optional debug views to show intermediate output. @@ -56,9 +57,22 @@ import java.util.concurrent.atomic.AtomicInteger; Context context, int outputWidth, int outputHeight, + float pixelWidthHeightRatio, Matrix transformationMatrix, Surface outputSurface, - Transformer.DebugViewProvider debugViewProvider) { + Transformer.DebugViewProvider debugViewProvider) + throws TransformationException { + if (pixelWidthHeightRatio != 1.0f) { + // TODO(http://b/211782176): Consider implementing support for non-square pixels. + throw new TransformationException( + "FrameEditor Error", + new IllegalArgumentException( + "Transformer's frame editor currently does not support frame edits on non-square" + + " pixels. The pixelWidthHeightRatio is: " + + pixelWidthHeightRatio), + TransformationException.ERROR_CODE_GL_INIT_FAILED); + } + EGLDisplay eglDisplay = GlUtil.createEglDisplay(); EGLContext eglContext = GlUtil.createEglContext(eglDisplay); EGLSurface eglSurface = GlUtil.getEglSurface(eglDisplay, outputSurface); diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java index 524802aa02..c385ebfb40 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java @@ -286,7 +286,7 @@ public final class TransformationException extends Exception { * Returns whether the error data associated to this exception equals the error data associated to * {@code other}. * - *

          Note that this method does not compare the exceptions' stacktraces. + *

          Note that this method does not compare the exceptions' stack traces. */ public boolean errorInfoEquals(@Nullable TransformationException other) { if (this == other) { diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java index d8f83d4aa0..74f9fb237a 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java @@ -104,6 +104,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; context, outputWidth, outputHeight, + inputFormat.pixelWidthHeightRatio, transformationRequest.transformationMatrix, /* outputSurface= */ checkNotNull(encoder.getInputSurface()), debugViewProvider); From de191770d54b6cdeb8ac781c6ff70dc3c51807c2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 Dec 2021 12:57:16 +0000 Subject: [PATCH 47/56] Fix 1 ErrorProneStyle finding: * These grouping parentheses are unnecessary; it is unlikely the code will be misinterpreted without them PiperOrigin-RevId: 417988060 --- .../android/exoplayer2/source/dash/PlayerEmsgHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java index f368bdc071..f60bd0563e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -135,7 +135,7 @@ public final class PlayerEmsgHandler implements Handler.Callback { return true; } switch (message.what) { - case (EMSG_MANIFEST_EXPIRED): + case EMSG_MANIFEST_EXPIRED: ManifestExpiryEventInfo messageObj = (ManifestExpiryEventInfo) message.obj; handleManifestExpiredMessage( messageObj.eventTimeUs, messageObj.manifestPublishTimeMsInEmsg); From 8b1190275223dfde64919c965c738852518779a5 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Thu, 23 Dec 2021 15:38:36 +0000 Subject: [PATCH 48/56] Throw when inferred sample MIME type is not supported by the muxer. This is better than silently dropping tracks as done previously. Later, we will implement fallback to transcoding to a supported MIME type. PiperOrigin-RevId: 418006258 --- .../transformer/TransformationException.java | 24 + .../exoplayer2/transformer/Transformer.java | 6 +- .../transformer/TransformerBaseRenderer.java | 85 +- .../transformer/TransformerTest.java | 103 +- .../amr/sample_nb.amr.aac.dump | 1315 +++++++++++++++++ .../mkv/sample_with_srt.mkv.dump | 237 ++- .../mp4/sample_18byte_nclx_colr.mp4.dump | 198 +++ 7 files changed, 1883 insertions(+), 85 deletions(-) create mode 100644 testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.aac.dump create mode 100644 testdata/src/test/assets/transformerdumps/mp4/sample_18byte_nclx_colr.mp4.dump diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java index c385ebfb40..0e56881c72 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java @@ -159,6 +159,16 @@ public final class TransformationException extends Exception { /** Caused by an audio processor initialization failure. */ public static final int ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED = 6001; + // Muxing errors (7xxx). + + /** + * Caused by an output sample MIME type inferred from the input not being supported by the muxer. + * + *

          Use {@link TransformationRequest.Builder#setAudioMimeType(String)} or {@link + * TransformationRequest.Builder#setVideoMimeType(String)} to transcode to a supported MIME type. + */ + public static final int ERROR_CODE_MUXER_SAMPLE_MIME_TYPE_UNSUPPORTED = 7001; + private static final ImmutableBiMap NAME_TO_ERROR_CODE = new ImmutableBiMap.Builder() .put("ERROR_CODE_FAILED_RUNTIME_CHECK", ERROR_CODE_FAILED_RUNTIME_CHECK) @@ -180,6 +190,9 @@ public final class TransformationException extends Exception { .put("ERROR_CODE_GL_INIT_FAILED", ERROR_CODE_GL_INIT_FAILED) .put("ERROR_CODE_GL_PROCESSING_FAILED", ERROR_CODE_GL_PROCESSING_FAILED) .put("ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED", ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED) + .put( + "ERROR_CODE_MUXER_SAMPLE_MIME_TYPE_UNSUPPORTED", + ERROR_CODE_MUXER_SAMPLE_MIME_TYPE_UNSUPPORTED) .buildOrThrow(); /** Returns the {@code errorCode} for a given name. */ @@ -230,6 +243,17 @@ public final class TransformationException extends Exception { componentName + " error, audio_format = " + audioFormat, cause, errorCode); } + /** + * Creates an instance for a muxer related exception. + * + * @param cause The cause of the failure. + * @param errorCode See {@link #errorCode}. + * @return The created instance. + */ + /* package */ static TransformationException createForMuxer(Throwable cause, int errorCode) { + return new TransformationException("Muxer error", cause, errorCode); + } + /** * Creates an instance for an unexpected exception. * diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java index fc4a96957b..7f1e41807c 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java @@ -806,13 +806,9 @@ public final class Transformer { @Override public void onTracksInfoChanged(TracksInfo tracksInfo) { if (muxerWrapper.getTrackCount() == 0) { - // TODO(b/209469847): Do not silently drop unsupported tracks and throw a more specific - // exception earlier. handleTransformationEnded( TransformationException.createForUnexpected( - new IllegalStateException( - "The output does not contain any tracks. Check that at least one of the input" - + " sample formats is supported."))); + new IllegalStateException("The output does not contain any tracks."))); } } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java index 4dd8ed7775..a9909e16ea 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.transformer; +import static com.google.android.exoplayer2.util.Assertions.checkState; import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; import androidx.annotation.Nullable; @@ -57,26 +58,46 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; this.transformationRequest = transformationRequest; } + /** + * Returns whether the renderer supports the track type of the given input format. + * + * @param inputFormat The input format. + * @return The {@link Capabilities} for this format. + * @throws ExoPlaybackException If the muxer does not support the output sample MIME type derived + * from the input {@code format} and {@link TransformationRequest}. + */ @Override - @C.FormatSupport - public final int supportsFormat(Format format) { - @Nullable String sampleMimeType = format.sampleMimeType; - if (MimeTypes.getTrackType(sampleMimeType) != getTrackType()) { + @Capabilities + public final int supportsFormat(Format inputFormat) throws ExoPlaybackException { + @Nullable String inputSampleMimeType = inputFormat.sampleMimeType; + if (inputSampleMimeType == null + || MimeTypes.getTrackType(inputSampleMimeType) != getTrackType()) { return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE); - } else if ((MimeTypes.isAudio(sampleMimeType) - && muxerWrapper.supportsSampleMimeType( - transformationRequest.audioMimeType == null - ? sampleMimeType - : transformationRequest.audioMimeType)) - || (MimeTypes.isVideo(sampleMimeType) - && muxerWrapper.supportsSampleMimeType( - transformationRequest.videoMimeType == null - ? sampleMimeType - : transformationRequest.videoMimeType))) { - return RendererCapabilities.create(C.FORMAT_HANDLED); - } else { - return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE); } + + // If the output sample MIME type is given in the transformationRequest it has already been + // validated by the builder. + if (MimeTypes.isAudio(inputSampleMimeType) && transformationRequest.audioMimeType != null) { + checkState(muxerWrapper.supportsSampleMimeType(transformationRequest.audioMimeType)); + return RendererCapabilities.create(C.FORMAT_HANDLED); + } + if (MimeTypes.isVideo(inputSampleMimeType) && transformationRequest.videoMimeType != null) { + checkState(muxerWrapper.supportsSampleMimeType(transformationRequest.videoMimeType)); + return RendererCapabilities.create(C.FORMAT_HANDLED); + } + + // When the output sample MIME type is not given in the transformationRequest, it is inferred + // from the input. + if (muxerWrapper.supportsSampleMimeType(inputSampleMimeType)) { + return RendererCapabilities.create(C.FORMAT_HANDLED); + } + throw wrapTransformationException( + TransformationException.createForMuxer( + new IllegalArgumentException( + "The sample MIME inferred from the input is not supported by the muxer. " + + "Input sample MIME type: " + + inputSampleMimeType), + TransformationException.ERROR_CODE_MUXER_SAMPLE_MIME_TYPE_UNSUPPORTED)); } @Override @@ -103,16 +124,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; while (feedMuxerFromPipeline() || samplePipeline.processData() || feedPipelineFromInput()) {} } catch (TransformationException e) { - // Transformer extracts the TransformationException from this ExoPlaybackException again. This - // temporary wrapping is needed due to the dependence on ExoPlayer's BaseRenderer. - throw ExoPlaybackException.createForRenderer( - e, - "Transformer", - getIndex(), - /* rendererFormat= */ null, - C.FORMAT_HANDLED, - /* isRecoverable= */ false, - PlaybackException.ERROR_CODE_UNSPECIFIED); + throw wrapTransformationException(e); } } @@ -226,4 +238,23 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return false; } } + + /** + * Returns an {@link ExoPlaybackException} wrapping the {@link TransformationException}. + * + *

          This temporary wrapping is needed due to the dependence on ExoPlayer's BaseRenderer. {@link + * Transformer} extracts the {@link TransformationException} from this {@link + * ExoPlaybackException} again. + */ + private ExoPlaybackException wrapTransformationException( + TransformationException transformationException) { + return ExoPlaybackException.createForRenderer( + transformationException, + "Transformer", + getIndex(), + /* rendererFormat= */ null, + C.FORMAT_HANDLED, + /* isRecoverable= */ false, + PlaybackException.ERROR_CODE_UNSPECIFIED); + } } diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java index fdf965012e..6cc576ee2e 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java @@ -62,12 +62,14 @@ import org.robolectric.shadows.ShadowMediaCodec; public final class TransformerTest { private static final String URI_PREFIX = "asset:///media/"; - private static final String FILE_VIDEO_ONLY = "mkv/sample.mkv"; - private static final String FILE_AUDIO_ONLY = "amr/sample_nb.amr"; + private static final String FILE_VIDEO_ONLY = "mp4/sample_18byte_nclx_colr.mp4"; private static final String FILE_AUDIO_VIDEO = "mp4/sample.mp4"; private static final String FILE_WITH_SUBTITLES = "mkv/sample_with_srt.mkv"; private static final String FILE_WITH_SEF_SLOW_MOTION = "mp4/sample_sef_slow_motion.mp4"; - private static final String FILE_WITH_ALL_SAMPLE_FORMATS_UNSUPPORTED = "mp4/sample_ac3.mp4"; + private static final String FILE_AUDIO_UNSUPPORTED_BY_DECODER = "amr/sample_wb.amr"; + private static final String FILE_AUDIO_UNSUPPORTED_BY_ENCODER = "amr/sample_nb.amr"; + private static final String FILE_AUDIO_UNSUPPORTED_BY_MUXER = "mp4/sample_ac3.mp4"; + private static final String FILE_VIDEO_UNSUPPORTED = "vp9/bear-vp9.webm"; private static final String FILE_UNKNOWN_DURATION = "mp4/sample_fragmented.mp4"; public static final String DUMP_FILE_OUTPUT_DIRECTORY = "transformerdumps"; public static final String DUMP_FILE_EXTENSION = "dump"; @@ -94,7 +96,7 @@ public final class TransformerTest { } @Test - public void startTransformation_videoOnly_completesSuccessfully() throws Exception { + public void startTransformation_videoOnlyPassthrough_completesSuccessfully() throws Exception { Transformer transformer = new Transformer.Builder(context) .setClock(clock) @@ -109,18 +111,40 @@ public final class TransformerTest { } @Test - public void startTransformation_audioOnly_completesSuccessfully() throws Exception { + public void startTransformation_audioOnlyPassthrough_completesSuccessfully() throws Exception { Transformer transformer = new Transformer.Builder(context) .setClock(clock) .setMuxerFactory(new TestMuxerFactory()) .build(); - MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_ONLY); + + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_UNSUPPORTED_BY_ENCODER); transformer.startTransformation(mediaItem, outputPath); TransformerTestRunner.runUntilCompleted(transformer); - DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_ONLY)); + DumpFileAsserts.assertOutput( + context, testMuxer, getDumpFileName(FILE_AUDIO_UNSUPPORTED_BY_ENCODER)); + } + + @Test + public void startTransformation_audioOnlyTranscoding_completesSuccessfully() throws Exception { + Transformer transformer = + new Transformer.Builder(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .setTransformationRequest( + new TransformationRequest.Builder() + .setAudioMimeType(MimeTypes.AUDIO_AAC) // supported by encoder and muxer + .build()) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_UNSUPPORTED_BY_ENCODER); + + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + + DumpFileAsserts.assertOutput( + context, testMuxer, getDumpFileName(FILE_AUDIO_UNSUPPORTED_BY_ENCODER + ".aac")); } @Test @@ -144,6 +168,8 @@ public final class TransformerTest { new Transformer.Builder(context) .setClock(clock) .setMuxerFactory(new TestMuxerFactory()) + .setTransformationRequest( + new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AAC).build()) .build(); MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_WITH_SUBTITLES); @@ -161,7 +187,7 @@ public final class TransformerTest { .setClock(clock) .setMuxerFactory(new TestMuxerFactory()) .build(); - MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_VIDEO); // Transform first media item. transformer.startTransformation(mediaItem, outputPath); @@ -172,7 +198,7 @@ public final class TransformerTest { transformer.startTransformation(mediaItem, outputPath); TransformerTestRunner.runUntilCompleted(transformer); - DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_VIDEO_ONLY)); + DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO)); } @Test @@ -246,10 +272,11 @@ public final class TransformerTest { .setMuxerFactory(new TestMuxerFactory()) .setTransformationRequest( new TransformationRequest.Builder() - .setAudioMimeType(MimeTypes.AUDIO_AMR_WB) // unsupported encoder MIME type + .setAudioMimeType( + MimeTypes.AUDIO_AMR_NB) // unsupported by encoder, supported by muxer .build()) .build(); - MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_ONLY); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_UNSUPPORTED_BY_MUXER); transformer.startTransformation(mediaItem, outputPath); TransformationException exception = TransformerTestRunner.runUntilError(transformer); @@ -268,10 +295,10 @@ public final class TransformerTest { .setMuxerFactory(new TestMuxerFactory()) .setTransformationRequest( new TransformationRequest.Builder() - .setAudioMimeType(MimeTypes.AUDIO_AAC) // supported encoder MIME type + .setAudioMimeType(MimeTypes.AUDIO_AAC) // supported by encoder and muxer .build()) .build(); - MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_WITH_ALL_SAMPLE_FORMATS_UNSUPPORTED); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_UNSUPPORTED_BY_DECODER); transformer.startTransformation(mediaItem, outputPath); TransformationException exception = TransformerTestRunner.runUntilError(transformer); @@ -315,15 +342,41 @@ public final class TransformerTest { } @Test - public void startTransformation_withAllSampleFormatsUnsupported_completesWithError() + public void startTransformation_withAudioMuxerFormatUnsupported_completesWithError() throws Exception { - Transformer transformer = new Transformer.Builder(context).setClock(clock).build(); - MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_WITH_ALL_SAMPLE_FORMATS_UNSUPPORTED); + Transformer transformer = + new Transformer.Builder(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_UNSUPPORTED_BY_MUXER); transformer.startTransformation(mediaItem, outputPath); TransformationException exception = TransformerTestRunner.runUntilError(transformer); - assertThat(exception).hasCauseThat().isInstanceOf(IllegalStateException.class); + assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(exception).hasCauseThat().hasMessageThat().contains("audio"); + assertThat(exception.errorCode) + .isEqualTo(TransformationException.ERROR_CODE_MUXER_SAMPLE_MIME_TYPE_UNSUPPORTED); + } + + @Test + public void startTransformation_withVideoMuxerFormatUnsupported_completesWithError() + throws Exception { + Transformer transformer = + new Transformer.Builder(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_UNSUPPORTED); + + transformer.startTransformation(mediaItem, outputPath); + TransformationException exception = TransformerTestRunner.runUntilError(transformer); + + assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(exception).hasCauseThat().hasMessageThat().contains("video"); + assertThat(exception.errorCode) + .isEqualTo(TransformationException.ERROR_CODE_MUXER_SAMPLE_MIME_TYPE_UNSUPPORTED); } @Test @@ -333,7 +386,7 @@ public final class TransformerTest { .setClock(clock) .setMuxerFactory(new TestMuxerFactory()) .build(); - MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_VIDEO); transformer.startTransformation(mediaItem, outputPath); transformer.cancel(); @@ -343,7 +396,7 @@ public final class TransformerTest { transformer.startTransformation(mediaItem, outputPath); TransformerTestRunner.runUntilCompleted(transformer); - DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_VIDEO_ONLY)); + DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO)); } @Test @@ -357,7 +410,7 @@ public final class TransformerTest { .setClock(clock) .setMuxerFactory(new TestMuxerFactory()) .build(); - MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_ONLY); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_VIDEO); AtomicReference exception = new AtomicReference<>(); CountDownLatch countDownLatch = new CountDownLatch(1); @@ -376,13 +429,13 @@ public final class TransformerTest { countDownLatch.await(); assertThat(exception.get()).isNull(); - DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_ONLY)); + DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO)); } @Test public void startTransformation_fromWrongThread_throwsError() throws Exception { Transformer transformer = new Transformer.Builder(context).setClock(clock).build(); - MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_ONLY); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_VIDEO); HandlerThread anotherThread = new HandlerThread("AnotherThread"); AtomicReference illegalStateException = new AtomicReference<>(); CountDownLatch countDownLatch = new CountDownLatch(1); @@ -609,6 +662,7 @@ public final class TransformerTest { /* outputBufferSize= */ 10_000, /* codec= */ (in, out) -> out.put(in)); ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AAC, codecConfig); + ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AC3, codecConfig); ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AMR_NB, codecConfig); ShadowMediaCodec.addEncoder(MimeTypes.AUDIO_AAC, codecConfig); @@ -632,8 +686,9 @@ public final class TransformerTest { throw new IllegalArgumentException("Format unsupported"); } }); - ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AC3, throwingCodecConfig); - ShadowMediaCodec.addEncoder(MimeTypes.AUDIO_AMR_WB, throwingCodecConfig); + + ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AMR_WB, throwingCodecConfig); + ShadowMediaCodec.addEncoder(MimeTypes.AUDIO_AMR_NB, throwingCodecConfig); ShadowMediaCodec.addEncoder(MimeTypes.VIDEO_H263, throwingCodecConfig); } diff --git a/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.aac.dump b/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.aac.dump new file mode 100644 index 0000000000..e885cab915 --- /dev/null +++ b/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.aac.dump @@ -0,0 +1,1315 @@ +containerMimeType = video/mp4 +format 0: + sampleMimeType = audio/mp4a-latm + channelCount = 1 + sampleRate = 8000 + pcmEncoding = 2 +sample: + trackIndex = 0 + dataHashCode = 924517484 + size = 13 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackIndex = 0 + dataHashCode = -835666085 + size = 13 + isKeyFrame = true + presentationTimeUs = 813 +sample: + trackIndex = 0 + dataHashCode = 430283125 + size = 13 + isKeyFrame = true + presentationTimeUs = 1625 +sample: + trackIndex = 0 + dataHashCode = 1215919932 + size = 13 + isKeyFrame = true + presentationTimeUs = 2438 +sample: + trackIndex = 0 + dataHashCode = -386387943 + size = 13 + isKeyFrame = true + presentationTimeUs = 3250 +sample: + trackIndex = 0 + dataHashCode = -765080119 + size = 13 + isKeyFrame = true + presentationTimeUs = 4063 +sample: + trackIndex = 0 + dataHashCode = -1855636054 + size = 13 + isKeyFrame = true + presentationTimeUs = 4875 +sample: + trackIndex = 0 + dataHashCode = -946579722 + size = 13 + isKeyFrame = true + presentationTimeUs = 5688 +sample: + trackIndex = 0 + dataHashCode = -841202654 + size = 13 + isKeyFrame = true + presentationTimeUs = 6500 +sample: + trackIndex = 0 + dataHashCode = -638764303 + size = 13 + isKeyFrame = true + presentationTimeUs = 7313 +sample: + trackIndex = 0 + dataHashCode = -1162388941 + size = 13 + isKeyFrame = true + presentationTimeUs = 8125 +sample: + trackIndex = 0 + dataHashCode = 572634367 + size = 13 + isKeyFrame = true + presentationTimeUs = 8938 +sample: + trackIndex = 0 + dataHashCode = -1774188021 + size = 13 + isKeyFrame = true + presentationTimeUs = 9750 +sample: + trackIndex = 0 + dataHashCode = 92464891 + size = 13 + isKeyFrame = true + presentationTimeUs = 10563 +sample: + trackIndex = 0 + dataHashCode = -991397659 + size = 13 + isKeyFrame = true + presentationTimeUs = 11375 +sample: + trackIndex = 0 + dataHashCode = -934698563 + size = 13 + isKeyFrame = true + presentationTimeUs = 12188 +sample: + trackIndex = 0 + dataHashCode = -811030035 + size = 13 + isKeyFrame = true + presentationTimeUs = 13000 +sample: + trackIndex = 0 + dataHashCode = 1892305159 + size = 13 + isKeyFrame = true + presentationTimeUs = 13813 +sample: + trackIndex = 0 + dataHashCode = -1266858924 + size = 13 + isKeyFrame = true + presentationTimeUs = 14625 +sample: + trackIndex = 0 + dataHashCode = 673814721 + size = 13 + isKeyFrame = true + presentationTimeUs = 15438 +sample: + trackIndex = 0 + dataHashCode = 1061124709 + size = 13 + isKeyFrame = true + presentationTimeUs = 16250 +sample: + trackIndex = 0 + dataHashCode = -869356712 + size = 13 + isKeyFrame = true + presentationTimeUs = 17063 +sample: + trackIndex = 0 + dataHashCode = 664729362 + size = 13 + isKeyFrame = true + presentationTimeUs = 17875 +sample: + trackIndex = 0 + dataHashCode = -1439741143 + size = 13 + isKeyFrame = true + presentationTimeUs = 18688 +sample: + trackIndex = 0 + dataHashCode = -151627580 + size = 13 + isKeyFrame = true + presentationTimeUs = 19500 +sample: + trackIndex = 0 + dataHashCode = -673268457 + size = 13 + isKeyFrame = true + presentationTimeUs = 20313 +sample: + trackIndex = 0 + dataHashCode = 1839962647 + size = 13 + isKeyFrame = true + presentationTimeUs = 21125 +sample: + trackIndex = 0 + dataHashCode = 1858999665 + size = 13 + isKeyFrame = true + presentationTimeUs = 21938 +sample: + trackIndex = 0 + dataHashCode = -1278193537 + size = 13 + isKeyFrame = true + presentationTimeUs = 22750 +sample: + trackIndex = 0 + dataHashCode = 568547001 + size = 13 + isKeyFrame = true + presentationTimeUs = 23563 +sample: + trackIndex = 0 + dataHashCode = 68217362 + size = 13 + isKeyFrame = true + presentationTimeUs = 24375 +sample: + trackIndex = 0 + dataHashCode = 1396217256 + size = 13 + isKeyFrame = true + presentationTimeUs = 25188 +sample: + trackIndex = 0 + dataHashCode = -971293094 + size = 13 + isKeyFrame = true + presentationTimeUs = 26000 +sample: + trackIndex = 0 + dataHashCode = -1742638874 + size = 13 + isKeyFrame = true + presentationTimeUs = 26813 +sample: + trackIndex = 0 + dataHashCode = 2047109317 + size = 13 + isKeyFrame = true + presentationTimeUs = 27625 +sample: + trackIndex = 0 + dataHashCode = -1668945241 + size = 13 + isKeyFrame = true + presentationTimeUs = 28438 +sample: + trackIndex = 0 + dataHashCode = -1229766218 + size = 13 + isKeyFrame = true + presentationTimeUs = 29250 +sample: + trackIndex = 0 + dataHashCode = 1765233454 + size = 13 + isKeyFrame = true + presentationTimeUs = 30063 +sample: + trackIndex = 0 + dataHashCode = -1930255456 + size = 13 + isKeyFrame = true + presentationTimeUs = 30875 +sample: + trackIndex = 0 + dataHashCode = -764925242 + size = 13 + isKeyFrame = true + presentationTimeUs = 31688 +sample: + trackIndex = 0 + dataHashCode = -1144688369 + size = 13 + isKeyFrame = true + presentationTimeUs = 32500 +sample: + trackIndex = 0 + dataHashCode = 1493699436 + size = 13 + isKeyFrame = true + presentationTimeUs = 33313 +sample: + trackIndex = 0 + dataHashCode = -468614511 + size = 13 + isKeyFrame = true + presentationTimeUs = 34125 +sample: + trackIndex = 0 + dataHashCode = -1578782058 + size = 13 + isKeyFrame = true + presentationTimeUs = 34938 +sample: + trackIndex = 0 + dataHashCode = -675743397 + size = 13 + isKeyFrame = true + presentationTimeUs = 35750 +sample: + trackIndex = 0 + dataHashCode = -863790111 + size = 13 + isKeyFrame = true + presentationTimeUs = 36563 +sample: + trackIndex = 0 + dataHashCode = -732307506 + size = 13 + isKeyFrame = true + presentationTimeUs = 37375 +sample: + trackIndex = 0 + dataHashCode = -693298708 + size = 13 + isKeyFrame = true + presentationTimeUs = 38188 +sample: + trackIndex = 0 + dataHashCode = -799131843 + size = 13 + isKeyFrame = true + presentationTimeUs = 39000 +sample: + trackIndex = 0 + dataHashCode = 1782866119 + size = 13 + isKeyFrame = true + presentationTimeUs = 39813 +sample: + trackIndex = 0 + dataHashCode = -912205505 + size = 13 + isKeyFrame = true + presentationTimeUs = 40625 +sample: + trackIndex = 0 + dataHashCode = 1067981287 + size = 13 + isKeyFrame = true + presentationTimeUs = 41438 +sample: + trackIndex = 0 + dataHashCode = 490520060 + size = 13 + isKeyFrame = true + presentationTimeUs = 42250 +sample: + trackIndex = 0 + dataHashCode = -1950632957 + size = 13 + isKeyFrame = true + presentationTimeUs = 43063 +sample: + trackIndex = 0 + dataHashCode = 565485817 + size = 13 + isKeyFrame = true + presentationTimeUs = 43875 +sample: + trackIndex = 0 + dataHashCode = -1057414703 + size = 13 + isKeyFrame = true + presentationTimeUs = 44688 +sample: + trackIndex = 0 + dataHashCode = 1568746155 + size = 13 + isKeyFrame = true + presentationTimeUs = 45500 +sample: + trackIndex = 0 + dataHashCode = 1355412472 + size = 13 + isKeyFrame = true + presentationTimeUs = 46313 +sample: + trackIndex = 0 + dataHashCode = 1546368465 + size = 13 + isKeyFrame = true + presentationTimeUs = 47125 +sample: + trackIndex = 0 + dataHashCode = 1811529381 + size = 13 + isKeyFrame = true + presentationTimeUs = 47938 +sample: + trackIndex = 0 + dataHashCode = 658031078 + size = 13 + isKeyFrame = true + presentationTimeUs = 48750 +sample: + trackIndex = 0 + dataHashCode = 1606584486 + size = 13 + isKeyFrame = true + presentationTimeUs = 49563 +sample: + trackIndex = 0 + dataHashCode = 2123252778 + size = 13 + isKeyFrame = true + presentationTimeUs = 50375 +sample: + trackIndex = 0 + dataHashCode = -1364579398 + size = 13 + isKeyFrame = true + presentationTimeUs = 51188 +sample: + trackIndex = 0 + dataHashCode = 1311427887 + size = 13 + isKeyFrame = true + presentationTimeUs = 52000 +sample: + trackIndex = 0 + dataHashCode = -691467569 + size = 13 + isKeyFrame = true + presentationTimeUs = 52813 +sample: + trackIndex = 0 + dataHashCode = 1876470084 + size = 13 + isKeyFrame = true + presentationTimeUs = 53625 +sample: + trackIndex = 0 + dataHashCode = -1472873479 + size = 13 + isKeyFrame = true + presentationTimeUs = 54438 +sample: + trackIndex = 0 + dataHashCode = -143574992 + size = 13 + isKeyFrame = true + presentationTimeUs = 55250 +sample: + trackIndex = 0 + dataHashCode = 984180453 + size = 13 + isKeyFrame = true + presentationTimeUs = 56063 +sample: + trackIndex = 0 + dataHashCode = -113645527 + size = 13 + isKeyFrame = true + presentationTimeUs = 56875 +sample: + trackIndex = 0 + dataHashCode = 1987501641 + size = 13 + isKeyFrame = true + presentationTimeUs = 57688 +sample: + trackIndex = 0 + dataHashCode = -1816426230 + size = 13 + isKeyFrame = true + presentationTimeUs = 58500 +sample: + trackIndex = 0 + dataHashCode = -1250050360 + size = 13 + isKeyFrame = true + presentationTimeUs = 59313 +sample: + trackIndex = 0 + dataHashCode = 1722852790 + size = 13 + isKeyFrame = true + presentationTimeUs = 60125 +sample: + trackIndex = 0 + dataHashCode = 225656333 + size = 13 + isKeyFrame = true + presentationTimeUs = 60938 +sample: + trackIndex = 0 + dataHashCode = -2137778394 + size = 13 + isKeyFrame = true + presentationTimeUs = 61750 +sample: + trackIndex = 0 + dataHashCode = 1433327155 + size = 13 + isKeyFrame = true + presentationTimeUs = 62563 +sample: + trackIndex = 0 + dataHashCode = -974261023 + size = 13 + isKeyFrame = true + presentationTimeUs = 63375 +sample: + trackIndex = 0 + dataHashCode = 1797813317 + size = 13 + isKeyFrame = true + presentationTimeUs = 64188 +sample: + trackIndex = 0 + dataHashCode = -594033497 + size = 13 + isKeyFrame = true + presentationTimeUs = 65000 +sample: + trackIndex = 0 + dataHashCode = -628310540 + size = 13 + isKeyFrame = true + presentationTimeUs = 65813 +sample: + trackIndex = 0 + dataHashCode = 1868627831 + size = 13 + isKeyFrame = true + presentationTimeUs = 66625 +sample: + trackIndex = 0 + dataHashCode = 1051863958 + size = 13 + isKeyFrame = true + presentationTimeUs = 67438 +sample: + trackIndex = 0 + dataHashCode = -1279059211 + size = 13 + isKeyFrame = true + presentationTimeUs = 68250 +sample: + trackIndex = 0 + dataHashCode = 408201874 + size = 13 + isKeyFrame = true + presentationTimeUs = 69063 +sample: + trackIndex = 0 + dataHashCode = 1686644299 + size = 13 + isKeyFrame = true + presentationTimeUs = 69875 +sample: + trackIndex = 0 + dataHashCode = 1288226241 + size = 13 + isKeyFrame = true + presentationTimeUs = 70688 +sample: + trackIndex = 0 + dataHashCode = 432829731 + size = 13 + isKeyFrame = true + presentationTimeUs = 71500 +sample: + trackIndex = 0 + dataHashCode = -1679312600 + size = 13 + isKeyFrame = true + presentationTimeUs = 72313 +sample: + trackIndex = 0 + dataHashCode = 1206680829 + size = 13 + isKeyFrame = true + presentationTimeUs = 73125 +sample: + trackIndex = 0 + dataHashCode = -325844704 + size = 13 + isKeyFrame = true + presentationTimeUs = 73938 +sample: + trackIndex = 0 + dataHashCode = 1941808848 + size = 13 + isKeyFrame = true + presentationTimeUs = 74750 +sample: + trackIndex = 0 + dataHashCode = -87346412 + size = 13 + isKeyFrame = true + presentationTimeUs = 75563 +sample: + trackIndex = 0 + dataHashCode = -329133765 + size = 13 + isKeyFrame = true + presentationTimeUs = 76375 +sample: + trackIndex = 0 + dataHashCode = -1299416212 + size = 13 + isKeyFrame = true + presentationTimeUs = 77188 +sample: + trackIndex = 0 + dataHashCode = -1314599219 + size = 13 + isKeyFrame = true + presentationTimeUs = 78000 +sample: + trackIndex = 0 + dataHashCode = 1456741286 + size = 13 + isKeyFrame = true + presentationTimeUs = 78813 +sample: + trackIndex = 0 + dataHashCode = 151296500 + size = 13 + isKeyFrame = true + presentationTimeUs = 79625 +sample: + trackIndex = 0 + dataHashCode = 1708763603 + size = 13 + isKeyFrame = true + presentationTimeUs = 80438 +sample: + trackIndex = 0 + dataHashCode = 227542220 + size = 13 + isKeyFrame = true + presentationTimeUs = 81250 +sample: + trackIndex = 0 + dataHashCode = 1094305517 + size = 13 + isKeyFrame = true + presentationTimeUs = 82063 +sample: + trackIndex = 0 + dataHashCode = -990377604 + size = 13 + isKeyFrame = true + presentationTimeUs = 82875 +sample: + trackIndex = 0 + dataHashCode = -1798036230 + size = 13 + isKeyFrame = true + presentationTimeUs = 83688 +sample: + trackIndex = 0 + dataHashCode = -1027148291 + size = 13 + isKeyFrame = true + presentationTimeUs = 84500 +sample: + trackIndex = 0 + dataHashCode = 359763976 + size = 13 + isKeyFrame = true + presentationTimeUs = 85313 +sample: + trackIndex = 0 + dataHashCode = 1332016420 + size = 13 + isKeyFrame = true + presentationTimeUs = 86125 +sample: + trackIndex = 0 + dataHashCode = -102753250 + size = 13 + isKeyFrame = true + presentationTimeUs = 86938 +sample: + trackIndex = 0 + dataHashCode = 1959063156 + size = 13 + isKeyFrame = true + presentationTimeUs = 87750 +sample: + trackIndex = 0 + dataHashCode = 2129089853 + size = 13 + isKeyFrame = true + presentationTimeUs = 88563 +sample: + trackIndex = 0 + dataHashCode = 1658742073 + size = 13 + isKeyFrame = true + presentationTimeUs = 89375 +sample: + trackIndex = 0 + dataHashCode = 2136916514 + size = 13 + isKeyFrame = true + presentationTimeUs = 90188 +sample: + trackIndex = 0 + dataHashCode = 105121407 + size = 13 + isKeyFrame = true + presentationTimeUs = 91000 +sample: + trackIndex = 0 + dataHashCode = -839464484 + size = 13 + isKeyFrame = true + presentationTimeUs = 91813 +sample: + trackIndex = 0 + dataHashCode = -1956791168 + size = 13 + isKeyFrame = true + presentationTimeUs = 92625 +sample: + trackIndex = 0 + dataHashCode = -1387546109 + size = 13 + isKeyFrame = true + presentationTimeUs = 93438 +sample: + trackIndex = 0 + dataHashCode = 128410432 + size = 13 + isKeyFrame = true + presentationTimeUs = 94250 +sample: + trackIndex = 0 + dataHashCode = 907081136 + size = 13 + isKeyFrame = true + presentationTimeUs = 95063 +sample: + trackIndex = 0 + dataHashCode = 1124845067 + size = 13 + isKeyFrame = true + presentationTimeUs = 95875 +sample: + trackIndex = 0 + dataHashCode = -1714479962 + size = 13 + isKeyFrame = true + presentationTimeUs = 96688 +sample: + trackIndex = 0 + dataHashCode = 322029323 + size = 13 + isKeyFrame = true + presentationTimeUs = 97500 +sample: + trackIndex = 0 + dataHashCode = -1116281187 + size = 13 + isKeyFrame = true + presentationTimeUs = 98313 +sample: + trackIndex = 0 + dataHashCode = 1571181228 + size = 13 + isKeyFrame = true + presentationTimeUs = 99125 +sample: + trackIndex = 0 + dataHashCode = 997979854 + size = 13 + isKeyFrame = true + presentationTimeUs = 99938 +sample: + trackIndex = 0 + dataHashCode = -1413492413 + size = 13 + isKeyFrame = true + presentationTimeUs = 100750 +sample: + trackIndex = 0 + dataHashCode = -381390490 + size = 13 + isKeyFrame = true + presentationTimeUs = 101563 +sample: + trackIndex = 0 + dataHashCode = -331348340 + size = 13 + isKeyFrame = true + presentationTimeUs = 102375 +sample: + trackIndex = 0 + dataHashCode = -1568238592 + size = 13 + isKeyFrame = true + presentationTimeUs = 103188 +sample: + trackIndex = 0 + dataHashCode = -941591445 + size = 13 + isKeyFrame = true + presentationTimeUs = 104000 +sample: + trackIndex = 0 + dataHashCode = 1616911281 + size = 13 + isKeyFrame = true + presentationTimeUs = 104813 +sample: + trackIndex = 0 + dataHashCode = -1755664741 + size = 13 + isKeyFrame = true + presentationTimeUs = 105625 +sample: + trackIndex = 0 + dataHashCode = -1950609742 + size = 13 + isKeyFrame = true + presentationTimeUs = 106438 +sample: + trackIndex = 0 + dataHashCode = 1476082149 + size = 13 + isKeyFrame = true + presentationTimeUs = 107250 +sample: + trackIndex = 0 + dataHashCode = 1289547483 + size = 13 + isKeyFrame = true + presentationTimeUs = 108063 +sample: + trackIndex = 0 + dataHashCode = -367599018 + size = 13 + isKeyFrame = true + presentationTimeUs = 108875 +sample: + trackIndex = 0 + dataHashCode = 679378334 + size = 13 + isKeyFrame = true + presentationTimeUs = 109688 +sample: + trackIndex = 0 + dataHashCode = 1437306809 + size = 13 + isKeyFrame = true + presentationTimeUs = 110500 +sample: + trackIndex = 0 + dataHashCode = 311988463 + size = 13 + isKeyFrame = true + presentationTimeUs = 111313 +sample: + trackIndex = 0 + dataHashCode = -1870442665 + size = 13 + isKeyFrame = true + presentationTimeUs = 112125 +sample: + trackIndex = 0 + dataHashCode = 1530013920 + size = 13 + isKeyFrame = true + presentationTimeUs = 112938 +sample: + trackIndex = 0 + dataHashCode = -585506443 + size = 13 + isKeyFrame = true + presentationTimeUs = 113750 +sample: + trackIndex = 0 + dataHashCode = -293690558 + size = 13 + isKeyFrame = true + presentationTimeUs = 114563 +sample: + trackIndex = 0 + dataHashCode = -616893325 + size = 13 + isKeyFrame = true + presentationTimeUs = 115375 +sample: + trackIndex = 0 + dataHashCode = 632210495 + size = 13 + isKeyFrame = true + presentationTimeUs = 116188 +sample: + trackIndex = 0 + dataHashCode = -291767937 + size = 13 + isKeyFrame = true + presentationTimeUs = 117000 +sample: + trackIndex = 0 + dataHashCode = -270265 + size = 13 + isKeyFrame = true + presentationTimeUs = 117813 +sample: + trackIndex = 0 + dataHashCode = -1095959376 + size = 13 + isKeyFrame = true + presentationTimeUs = 118625 +sample: + trackIndex = 0 + dataHashCode = -1363867284 + size = 13 + isKeyFrame = true + presentationTimeUs = 119438 +sample: + trackIndex = 0 + dataHashCode = 185415707 + size = 13 + isKeyFrame = true + presentationTimeUs = 120250 +sample: + trackIndex = 0 + dataHashCode = 1033720098 + size = 13 + isKeyFrame = true + presentationTimeUs = 121063 +sample: + trackIndex = 0 + dataHashCode = 1813896085 + size = 13 + isKeyFrame = true + presentationTimeUs = 121875 +sample: + trackIndex = 0 + dataHashCode = -1381192241 + size = 13 + isKeyFrame = true + presentationTimeUs = 122688 +sample: + trackIndex = 0 + dataHashCode = 362689054 + size = 13 + isKeyFrame = true + presentationTimeUs = 123500 +sample: + trackIndex = 0 + dataHashCode = -1320787356 + size = 13 + isKeyFrame = true + presentationTimeUs = 124313 +sample: + trackIndex = 0 + dataHashCode = 1306489379 + size = 13 + isKeyFrame = true + presentationTimeUs = 125125 +sample: + trackIndex = 0 + dataHashCode = -910313430 + size = 13 + isKeyFrame = true + presentationTimeUs = 125938 +sample: + trackIndex = 0 + dataHashCode = -1533334115 + size = 13 + isKeyFrame = true + presentationTimeUs = 126750 +sample: + trackIndex = 0 + dataHashCode = -700061723 + size = 13 + isKeyFrame = true + presentationTimeUs = 127563 +sample: + trackIndex = 0 + dataHashCode = 474100444 + size = 13 + isKeyFrame = true + presentationTimeUs = 128375 +sample: + trackIndex = 0 + dataHashCode = -2096659943 + size = 13 + isKeyFrame = true + presentationTimeUs = 129188 +sample: + trackIndex = 0 + dataHashCode = -690442126 + size = 13 + isKeyFrame = true + presentationTimeUs = 130000 +sample: + trackIndex = 0 + dataHashCode = 158718784 + size = 13 + isKeyFrame = true + presentationTimeUs = 130813 +sample: + trackIndex = 0 + dataHashCode = -1587553019 + size = 13 + isKeyFrame = true + presentationTimeUs = 131625 +sample: + trackIndex = 0 + dataHashCode = 1266916929 + size = 13 + isKeyFrame = true + presentationTimeUs = 132438 +sample: + trackIndex = 0 + dataHashCode = 1947792537 + size = 13 + isKeyFrame = true + presentationTimeUs = 133250 +sample: + trackIndex = 0 + dataHashCode = 2051622372 + size = 13 + isKeyFrame = true + presentationTimeUs = 134063 +sample: + trackIndex = 0 + dataHashCode = 1648973196 + size = 13 + isKeyFrame = true + presentationTimeUs = 134875 +sample: + trackIndex = 0 + dataHashCode = -1119069213 + size = 13 + isKeyFrame = true + presentationTimeUs = 135688 +sample: + trackIndex = 0 + dataHashCode = -1162670307 + size = 13 + isKeyFrame = true + presentationTimeUs = 136500 +sample: + trackIndex = 0 + dataHashCode = 505180178 + size = 13 + isKeyFrame = true + presentationTimeUs = 137313 +sample: + trackIndex = 0 + dataHashCode = -1707111799 + size = 13 + isKeyFrame = true + presentationTimeUs = 138125 +sample: + trackIndex = 0 + dataHashCode = 549350779 + size = 13 + isKeyFrame = true + presentationTimeUs = 138938 +sample: + trackIndex = 0 + dataHashCode = -895461091 + size = 13 + isKeyFrame = true + presentationTimeUs = 139750 +sample: + trackIndex = 0 + dataHashCode = 1834306839 + size = 13 + isKeyFrame = true + presentationTimeUs = 140563 +sample: + trackIndex = 0 + dataHashCode = -646169807 + size = 13 + isKeyFrame = true + presentationTimeUs = 141375 +sample: + trackIndex = 0 + dataHashCode = 123454915 + size = 13 + isKeyFrame = true + presentationTimeUs = 142188 +sample: + trackIndex = 0 + dataHashCode = 2074179659 + size = 13 + isKeyFrame = true + presentationTimeUs = 143000 +sample: + trackIndex = 0 + dataHashCode = 488070546 + size = 13 + isKeyFrame = true + presentationTimeUs = 143813 +sample: + trackIndex = 0 + dataHashCode = -1379245827 + size = 13 + isKeyFrame = true + presentationTimeUs = 144625 +sample: + trackIndex = 0 + dataHashCode = 922846867 + size = 13 + isKeyFrame = true + presentationTimeUs = 145438 +sample: + trackIndex = 0 + dataHashCode = 1163092079 + size = 13 + isKeyFrame = true + presentationTimeUs = 146250 +sample: + trackIndex = 0 + dataHashCode = -817674907 + size = 13 + isKeyFrame = true + presentationTimeUs = 147063 +sample: + trackIndex = 0 + dataHashCode = -765143209 + size = 13 + isKeyFrame = true + presentationTimeUs = 147875 +sample: + trackIndex = 0 + dataHashCode = 1337234415 + size = 13 + isKeyFrame = true + presentationTimeUs = 148688 +sample: + trackIndex = 0 + dataHashCode = 152696122 + size = 13 + isKeyFrame = true + presentationTimeUs = 149500 +sample: + trackIndex = 0 + dataHashCode = -1037369189 + size = 13 + isKeyFrame = true + presentationTimeUs = 150313 +sample: + trackIndex = 0 + dataHashCode = 93852784 + size = 13 + isKeyFrame = true + presentationTimeUs = 151125 +sample: + trackIndex = 0 + dataHashCode = -1512860804 + size = 13 + isKeyFrame = true + presentationTimeUs = 151938 +sample: + trackIndex = 0 + dataHashCode = -1571797975 + size = 13 + isKeyFrame = true + presentationTimeUs = 152750 +sample: + trackIndex = 0 + dataHashCode = -1390710594 + size = 13 + isKeyFrame = true + presentationTimeUs = 153563 +sample: + trackIndex = 0 + dataHashCode = 775548254 + size = 13 + isKeyFrame = true + presentationTimeUs = 154375 +sample: + trackIndex = 0 + dataHashCode = 329825934 + size = 13 + isKeyFrame = true + presentationTimeUs = 155188 +sample: + trackIndex = 0 + dataHashCode = 449672203 + size = 13 + isKeyFrame = true + presentationTimeUs = 156000 +sample: + trackIndex = 0 + dataHashCode = 135215283 + size = 13 + isKeyFrame = true + presentationTimeUs = 156813 +sample: + trackIndex = 0 + dataHashCode = -627202145 + size = 13 + isKeyFrame = true + presentationTimeUs = 157625 +sample: + trackIndex = 0 + dataHashCode = 565795710 + size = 13 + isKeyFrame = true + presentationTimeUs = 158438 +sample: + trackIndex = 0 + dataHashCode = -853390981 + size = 13 + isKeyFrame = true + presentationTimeUs = 159250 +sample: + trackIndex = 0 + dataHashCode = 1904980829 + size = 13 + isKeyFrame = true + presentationTimeUs = 160063 +sample: + trackIndex = 0 + dataHashCode = 1772857005 + size = 13 + isKeyFrame = true + presentationTimeUs = 160875 +sample: + trackIndex = 0 + dataHashCode = -1159621303 + size = 13 + isKeyFrame = true + presentationTimeUs = 161688 +sample: + trackIndex = 0 + dataHashCode = 712585139 + size = 13 + isKeyFrame = true + presentationTimeUs = 162500 +sample: + trackIndex = 0 + dataHashCode = 7470296 + size = 13 + isKeyFrame = true + presentationTimeUs = 163313 +sample: + trackIndex = 0 + dataHashCode = 1154659763 + size = 13 + isKeyFrame = true + presentationTimeUs = 164125 +sample: + trackIndex = 0 + dataHashCode = 512209179 + size = 13 + isKeyFrame = true + presentationTimeUs = 164938 +sample: + trackIndex = 0 + dataHashCode = 2026712081 + size = 13 + isKeyFrame = true + presentationTimeUs = 165750 +sample: + trackIndex = 0 + dataHashCode = -1625715216 + size = 13 + isKeyFrame = true + presentationTimeUs = 166563 +sample: + trackIndex = 0 + dataHashCode = -1299058326 + size = 13 + isKeyFrame = true + presentationTimeUs = 167375 +sample: + trackIndex = 0 + dataHashCode = -813560096 + size = 13 + isKeyFrame = true + presentationTimeUs = 168188 +sample: + trackIndex = 0 + dataHashCode = 1311045251 + size = 13 + isKeyFrame = true + presentationTimeUs = 169000 +sample: + trackIndex = 0 + dataHashCode = 1388107407 + size = 13 + isKeyFrame = true + presentationTimeUs = 169813 +sample: + trackIndex = 0 + dataHashCode = 1113099440 + size = 13 + isKeyFrame = true + presentationTimeUs = 170625 +sample: + trackIndex = 0 + dataHashCode = -339743582 + size = 13 + isKeyFrame = true + presentationTimeUs = 171438 +sample: + trackIndex = 0 + dataHashCode = -1055895345 + size = 13 + isKeyFrame = true + presentationTimeUs = 172250 +sample: + trackIndex = 0 + dataHashCode = 1869841923 + size = 13 + isKeyFrame = true + presentationTimeUs = 173063 +sample: + trackIndex = 0 + dataHashCode = 229443301 + size = 13 + isKeyFrame = true + presentationTimeUs = 173875 +sample: + trackIndex = 0 + dataHashCode = 1526951012 + size = 13 + isKeyFrame = true + presentationTimeUs = 174688 +sample: + trackIndex = 0 + dataHashCode = -1517436626 + size = 13 + isKeyFrame = true + presentationTimeUs = 175500 +sample: + trackIndex = 0 + dataHashCode = -1403405700 + size = 13 + isKeyFrame = true + presentationTimeUs = 176313 +released = true diff --git a/testdata/src/test/assets/transformerdumps/mkv/sample_with_srt.mkv.dump b/testdata/src/test/assets/transformerdumps/mkv/sample_with_srt.mkv.dump index bf39e2d187..e8c74ad532 100644 --- a/testdata/src/test/assets/transformerdumps/mkv/sample_with_srt.mkv.dump +++ b/testdata/src/test/assets/transformerdumps/mkv/sample_with_srt.mkv.dump @@ -1,5 +1,10 @@ containerMimeType = video/mp4 format 0: + sampleMimeType = audio/mp4a-latm + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 +format 1: id = 1 sampleMimeType = video/avc codecs = avc1.640034 @@ -11,181 +16,355 @@ format 0: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -252482306 size = 36477 isKeyFrame = true presentationTimeUs = 0 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = 67864034 size = 5341 isKeyFrame = false presentationTimeUs = 67000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = 897273234 size = 596 isKeyFrame = false presentationTimeUs = 33000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -1549870586 size = 7704 isKeyFrame = false presentationTimeUs = 200000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = 672384813 size = 989 isKeyFrame = false presentationTimeUs = 133000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -988996493 size = 721 isKeyFrame = false presentationTimeUs = 100000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = 1711151377 size = 519 isKeyFrame = false presentationTimeUs = 167000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -506806036 size = 6160 isKeyFrame = false presentationTimeUs = 333000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = 1902167649 size = 953 isKeyFrame = false presentationTimeUs = 267000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = 2054873212 size = 620 isKeyFrame = false presentationTimeUs = 233000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = 1556608231 size = 405 isKeyFrame = false presentationTimeUs = 300000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -1648978019 size = 4852 isKeyFrame = false presentationTimeUs = 433000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -484808327 size = 547 isKeyFrame = false presentationTimeUs = 400000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -20706048 size = 570 isKeyFrame = false presentationTimeUs = 367000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = 2085064574 size = 5525 isKeyFrame = false presentationTimeUs = 567000 sample: trackIndex = 0 + dataHashCode = 555688582 + size = 416 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackIndex = 0 + dataHashCode = 2000837254 + size = 418 + isKeyFrame = true + presentationTimeUs = 4717 +sample: + trackIndex = 0 + dataHashCode = -1593942879 + size = 418 + isKeyFrame = true + presentationTimeUs = 9456 +sample: + trackIndex = 0 + dataHashCode = 587837542 + size = 418 + isKeyFrame = true + presentationTimeUs = 14196 +sample: + trackIndex = 0 + dataHashCode = -1836423877 + size = 418 + isKeyFrame = true + presentationTimeUs = 18935 +sample: + trackIndex = 0 + dataHashCode = 874705099 + size = 418 + isKeyFrame = true + presentationTimeUs = 23674 +sample: + trackIndex = 0 + dataHashCode = -269206181 + size = 418 + isKeyFrame = true + presentationTimeUs = 28413 +sample: + trackIndex = 0 + dataHashCode = -58682425 + size = 418 + isKeyFrame = true + presentationTimeUs = 33152 +sample: + trackIndex = 0 + dataHashCode = -859796970 + size = 418 + isKeyFrame = true + presentationTimeUs = 37892 +sample: + trackIndex = 0 + dataHashCode = 711911523 + size = 418 + isKeyFrame = true + presentationTimeUs = 42631 +sample: + trackIndex = 0 + dataHashCode = -694513071 + size = 418 + isKeyFrame = true + presentationTimeUs = 47370 +sample: + trackIndex = 0 + dataHashCode = -1124371059 + size = 418 + isKeyFrame = true + presentationTimeUs = 52109 +sample: + trackIndex = 0 + dataHashCode = 297166745 + size = 418 + isKeyFrame = true + presentationTimeUs = 56849 +sample: + trackIndex = 0 + dataHashCode = -937110638 + size = 418 + isKeyFrame = true + presentationTimeUs = 61588 +sample: + trackIndex = 0 + dataHashCode = -1050158990 + size = 418 + isKeyFrame = true + presentationTimeUs = 66327 +sample: + trackIndex = 0 + dataHashCode = 1109510229 + size = 418 + isKeyFrame = true + presentationTimeUs = 71066 +sample: + trackIndex = 0 + dataHashCode = 1297086772 + size = 418 + isKeyFrame = true + presentationTimeUs = 75805 +sample: + trackIndex = 0 + dataHashCode = -1739939803 + size = 418 + isKeyFrame = true + presentationTimeUs = 80545 +sample: + trackIndex = 0 + dataHashCode = -1149727930 + size = 418 + isKeyFrame = true + presentationTimeUs = 85284 +sample: + trackIndex = 0 + dataHashCode = -1627652713 + size = 418 + isKeyFrame = true + presentationTimeUs = 90023 +sample: + trackIndex = 0 + dataHashCode = -551926260 + size = 418 + isKeyFrame = true + presentationTimeUs = 94762 +sample: + trackIndex = 0 + dataHashCode = 45987178 + size = 418 + isKeyFrame = true + presentationTimeUs = 99502 +sample: + trackIndex = 0 + dataHashCode = -903675808 + size = 418 + isKeyFrame = true + presentationTimeUs = 104241 +sample: + trackIndex = 0 + dataHashCode = -755916991 + size = 418 + isKeyFrame = true + presentationTimeUs = 108980 +sample: + trackIndex = 0 + dataHashCode = -1355207303 + size = 418 + isKeyFrame = true + presentationTimeUs = 113719 +sample: + trackIndex = 0 + dataHashCode = -975703389 + size = 418 + isKeyFrame = true + presentationTimeUs = 118459 +sample: + trackIndex = 0 + dataHashCode = 1933194670 + size = 418 + isKeyFrame = true + presentationTimeUs = 123198 +sample: + trackIndex = 0 + dataHashCode = -565778989 + size = 418 + isKeyFrame = true + presentationTimeUs = 127937 +sample: + trackIndex = 0 + dataHashCode = 1454083383 + size = 418 + isKeyFrame = true + presentationTimeUs = 132676 +sample: + trackIndex = 1 dataHashCode = -637074022 size = 1082 isKeyFrame = false presentationTimeUs = 500000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -1824027029 size = 807 isKeyFrame = false presentationTimeUs = 467000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -1701945306 size = 744 isKeyFrame = false presentationTimeUs = 533000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -952425536 size = 4732 isKeyFrame = false presentationTimeUs = 700000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -1978031576 size = 1004 isKeyFrame = false presentationTimeUs = 633000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -2128215508 size = 794 isKeyFrame = false presentationTimeUs = 600000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -259850011 size = 645 isKeyFrame = false presentationTimeUs = 667000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = 1920983928 size = 2684 isKeyFrame = false presentationTimeUs = 833000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = 1100642337 size = 787 isKeyFrame = false presentationTimeUs = 767000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = 1544917830 size = 649 isKeyFrame = false presentationTimeUs = 733000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -116205995 size = 509 isKeyFrame = false presentationTimeUs = 800000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = 696343585 size = 1226 isKeyFrame = false presentationTimeUs = 967000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -644371190 size = 898 isKeyFrame = false presentationTimeUs = 900000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -1606273467 size = 476 isKeyFrame = false presentationTimeUs = 867000 sample: - trackIndex = 0 + trackIndex = 1 dataHashCode = -571265861 size = 486 isKeyFrame = false diff --git a/testdata/src/test/assets/transformerdumps/mp4/sample_18byte_nclx_colr.mp4.dump b/testdata/src/test/assets/transformerdumps/mp4/sample_18byte_nclx_colr.mp4.dump new file mode 100644 index 0000000000..b8105aafea --- /dev/null +++ b/testdata/src/test/assets/transformerdumps/mp4/sample_18byte_nclx_colr.mp4.dump @@ -0,0 +1,198 @@ +containerMimeType = video/mp4 +format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64001F + maxInputSize = 36722 + width = 1080 + height = 720 + frameRate = 29.970028 + colorInfo: + colorSpace = 1 + colorRange = 2 + colorTransfer = 3 + hdrStaticInfo = length 0, hash 0 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B +sample: + trackIndex = 0 + dataHashCode = -770308242 + size = 36692 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackIndex = 0 + dataHashCode = -732087136 + size = 5312 + isKeyFrame = false + presentationTimeUs = 66733 +sample: + trackIndex = 0 + dataHashCode = 468156717 + size = 599 + isKeyFrame = false + presentationTimeUs = 33366 +sample: + trackIndex = 0 + dataHashCode = 1150349584 + size = 7735 + isKeyFrame = false + presentationTimeUs = 200200 +sample: + trackIndex = 0 + dataHashCode = 1443582006 + size = 987 + isKeyFrame = false + presentationTimeUs = 133466 +sample: + trackIndex = 0 + dataHashCode = -310585145 + size = 673 + isKeyFrame = false + presentationTimeUs = 100100 +sample: + trackIndex = 0 + dataHashCode = 807460688 + size = 523 + isKeyFrame = false + presentationTimeUs = 166833 +sample: + trackIndex = 0 + dataHashCode = 1936487090 + size = 6061 + isKeyFrame = false + presentationTimeUs = 333666 +sample: + trackIndex = 0 + dataHashCode = -32297181 + size = 992 + isKeyFrame = false + presentationTimeUs = 266933 +sample: + trackIndex = 0 + dataHashCode = 1529616406 + size = 623 + isKeyFrame = false + presentationTimeUs = 233566 +sample: + trackIndex = 0 + dataHashCode = 1949198785 + size = 421 + isKeyFrame = false + presentationTimeUs = 300300 +sample: + trackIndex = 0 + dataHashCode = -147880287 + size = 4899 + isKeyFrame = false + presentationTimeUs = 433766 +sample: + trackIndex = 0 + dataHashCode = 1369083472 + size = 568 + isKeyFrame = false + presentationTimeUs = 400400 +sample: + trackIndex = 0 + dataHashCode = 965782073 + size = 620 + isKeyFrame = false + presentationTimeUs = 367033 +sample: + trackIndex = 0 + dataHashCode = -261176150 + size = 5450 + isKeyFrame = false + presentationTimeUs = 567233 +sample: + trackIndex = 0 + dataHashCode = -1830836678 + size = 1051 + isKeyFrame = false + presentationTimeUs = 500500 +sample: + trackIndex = 0 + dataHashCode = 1767407540 + size = 874 + isKeyFrame = false + presentationTimeUs = 467133 +sample: + trackIndex = 0 + dataHashCode = 918440283 + size = 781 + isKeyFrame = false + presentationTimeUs = 533866 +sample: + trackIndex = 0 + dataHashCode = -1408463661 + size = 4725 + isKeyFrame = false + presentationTimeUs = 700700 +sample: + trackIndex = 0 + dataHashCode = 1569455924 + size = 1022 + isKeyFrame = false + presentationTimeUs = 633966 +sample: + trackIndex = 0 + dataHashCode = -1723778407 + size = 790 + isKeyFrame = false + presentationTimeUs = 600600 +sample: + trackIndex = 0 + dataHashCode = 1578275472 + size = 610 + isKeyFrame = false + presentationTimeUs = 667333 +sample: + trackIndex = 0 + dataHashCode = 1989768395 + size = 2751 + isKeyFrame = false + presentationTimeUs = 834166 +sample: + trackIndex = 0 + dataHashCode = -1215674502 + size = 745 + isKeyFrame = false + presentationTimeUs = 767433 +sample: + trackIndex = 0 + dataHashCode = -814473606 + size = 621 + isKeyFrame = false + presentationTimeUs = 734066 +sample: + trackIndex = 0 + dataHashCode = 498370894 + size = 505 + isKeyFrame = false + presentationTimeUs = 800800 +sample: + trackIndex = 0 + dataHashCode = -1051506468 + size = 1268 + isKeyFrame = false + presentationTimeUs = 967633 +sample: + trackIndex = 0 + dataHashCode = -1025604144 + size = 880 + isKeyFrame = false + presentationTimeUs = 900900 +sample: + trackIndex = 0 + dataHashCode = -913586520 + size = 530 + isKeyFrame = false + presentationTimeUs = 867533 +sample: + trackIndex = 0 + dataHashCode = 1340459242 + size = 568 + isKeyFrame = false + presentationTimeUs = 934266 +released = true From e54e02fdcf60abebdd9944df58544575acabfce5 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 23 Dec 2021 17:40:14 +0000 Subject: [PATCH 49/56] Pre-allocate availableAllocations to prevent a re-size in release PiperOrigin-RevId: 418022431 --- .../exoplayer2/upstream/DefaultAllocator.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java index b79b66c930..47a77beb38 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java @@ -102,6 +102,12 @@ public final class DefaultAllocator implements Allocator { availableAllocations[availableCount] = null; } else { allocation = new Allocation(new byte[individualAllocationSize], 0); + if (allocatedCount > availableAllocations.length) { + // Make availableAllocations be large enough to contain all allocations made by this + // allocator so that release() does not need to grow the availableAllocations array. See + // [Internal ref: b/209801945]. + availableAllocations = Arrays.copyOf(availableAllocations, availableAllocations.length * 2); + } } return allocation; } @@ -114,12 +120,6 @@ public final class DefaultAllocator implements Allocator { @Override public synchronized void release(Allocation[] allocations) { - if (availableCount + allocations.length >= availableAllocations.length) { - availableAllocations = - Arrays.copyOf( - availableAllocations, - max(availableAllocations.length * 2, availableCount + allocations.length)); - } for (Allocation allocation : allocations) { availableAllocations[availableCount++] = allocation; } From aa467f52e474cc50ba65be26e1d3c7f65e81d840 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 27 Dec 2021 13:43:21 +0000 Subject: [PATCH 50/56] Make sure CastPlayer calls onIsPlaying if required Before this change we checked whether the playback state and playWhenReady have changed when the state from the cast device arrived. If we detected such a change we called the listener callback `onIsPlayingChanged`. However, in the case when `setPlayWhenReady(boolean)` is called on 'CastPlayer', we mask the change in `playWhenReady`, then send the play/pause request to the cast device and when the state from the cast device arrives we never detect a change because we have already masked `playWhenReady`. This change now moves the check for `isPlaying` to the same place where the state and playWhenReady is updated, so we call the `onIsPlayingChanged` callback in either case, when masking or when a changed state from the server arrives. Issue: google/ExoPlayer#9792 PiperOrigin-RevId: 418483509 --- RELEASENOTES.md | 3 ++ .../exoplayer2/ext/cast/CastPlayer.java | 12 +++--- .../exoplayer2/ext/cast/CastPlayerTest.java | 37 ++++++++++++++++--- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 42041d9020..7c1e42c991 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -86,6 +86,9 @@ * RTSP: * Provide a client API to override the `SocketFactory` used for any server connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)). +* Cast extension + * Fix bug that prevented `CastPlayer` from calling `onIsPlayingChanged` + correctly. * Remove deprecated symbols: * Remove `MediaSourceFactory#setDrmSessionManager`, `MediaSourceFactory#setDrmHttpDataSourceFactory`, and diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index ac8d4ea8c6..fbe5a31a93 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -816,13 +816,7 @@ public final class CastPlayer extends BasePlayer { !getCurrentTimeline().isEmpty() ? getCurrentTimeline().getPeriod(oldWindowIndex, period, /* setIds= */ true).uid : null; - boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady.value; updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null); - boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value; - if (wasPlaying != isPlaying) { - listeners.queueEvent( - Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying)); - } updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null); updatePlaybackRateAndNotifyIfChanged(/* resultCallback= */ null); boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged(); @@ -1216,6 +1210,7 @@ public final class CastPlayer extends BasePlayer { boolean playWhenReady, @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason, @Player.State int playbackState) { + boolean wasPlaying = this.playbackState == Player.STATE_READY && this.playWhenReady.value; boolean playWhenReadyChanged = this.playWhenReady.value != playWhenReady; boolean playbackStateChanged = this.playbackState != playbackState; if (playWhenReadyChanged || playbackStateChanged) { @@ -1234,6 +1229,11 @@ public final class CastPlayer extends BasePlayer { Player.EVENT_PLAY_WHEN_READY_CHANGED, listener -> listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason)); } + boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady; + if (wasPlaying != isPlaying) { + listeners.queueEvent( + Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying)); + } } } diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java index 917d25aba0..5b0e1a4fff 100644 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java @@ -52,6 +52,7 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -138,13 +139,18 @@ public class CastPlayerTest { @SuppressWarnings("deprecation") @Test public void setPlayWhenReady_masksRemoteState() { + when(mockRemoteMediaClient.getPlayerState()).thenReturn(MediaStatus.PLAYER_STATE_PLAYING); + // Trigger initial update to get out of STATE_IDLE to make onIsPlaying() be called. + remoteMediaClientCallback.onStatusUpdated(); + reset(mockListener); when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult); assertThat(castPlayer.getPlayWhenReady()).isFalse(); castPlayer.play(); verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture()); assertThat(castPlayer.getPlayWhenReady()).isTrue(); - verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); + verify(mockListener).onPlayerStateChanged(true, Player.STATE_READY); + verify(mockListener).onIsPlayingChanged(true); verify(mockListener) .onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); @@ -163,13 +169,18 @@ public class CastPlayerTest { @SuppressWarnings("deprecation") @Test public void setPlayWhenReadyMasking_updatesUponResultChange() { + when(mockRemoteMediaClient.getPlayerState()).thenReturn(MediaStatus.PLAYER_STATE_PLAYING); + // Trigger initial update to get out of STATE_IDLE to make onIsPlaying() be called. + remoteMediaClientCallback.onStatusUpdated(); + reset(mockListener); when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult); assertThat(castPlayer.getPlayWhenReady()).isFalse(); castPlayer.play(); verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture()); assertThat(castPlayer.getPlayWhenReady()).isTrue(); - verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); + verify(mockListener).onIsPlayingChanged(true); + verify(mockListener).onPlayerStateChanged(true, Player.STATE_READY); verify(mockListener) .onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); @@ -177,38 +188,52 @@ public class CastPlayerTest { setResultCallbackArgumentCaptor .getValue() .onResult(mock(RemoteMediaClient.MediaChannelResult.class)); - verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE); + verify(mockListener).onPlayerStateChanged(false, Player.STATE_READY); + verify(mockListener).onIsPlayingChanged(false); verify(mockListener).onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE); assertThat(castPlayer.getPlayWhenReady()).isFalse(); + verifyNoMoreInteractions(mockListener); } @SuppressWarnings("deprecation") @Test public void setPlayWhenReady_correctChangeReasonOnPause() { + when(mockRemoteMediaClient.getPlayerState()).thenReturn(MediaStatus.PLAYER_STATE_PLAYING); + // Trigger initial update to get out of STATE_IDLE to make onIsPlaying() be called. + remoteMediaClientCallback.onStatusUpdated(); + reset(mockListener); when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult); when(mockRemoteMediaClient.pause()).thenReturn(mockPendingResult); + castPlayer.play(); assertThat(castPlayer.getPlayWhenReady()).isTrue(); - verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); + verify(mockListener).onIsPlayingChanged(true); + verify(mockListener).onPlayerStateChanged(true, Player.STATE_READY); verify(mockListener) .onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); castPlayer.pause(); assertThat(castPlayer.getPlayWhenReady()).isFalse(); - verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE); + verify(mockListener).onIsPlayingChanged(false); verify(mockListener) .onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + verify(mockListener).onPlayerStateChanged(false, Player.STATE_READY); + verifyNoMoreInteractions(mockListener); } @SuppressWarnings("deprecation") @Test public void playWhenReady_changesOnStatusUpdates() { + when(mockRemoteMediaClient.getPlayerState()).thenReturn(MediaStatus.PLAYER_STATE_PLAYING); assertThat(castPlayer.getPlayWhenReady()).isFalse(); when(mockRemoteMediaClient.isPaused()).thenReturn(false); remoteMediaClientCallback.onStatusUpdated(); - verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); + verify(mockListener).onPlayerStateChanged(true, Player.STATE_READY); + verify(mockListener).onPlaybackStateChanged(Player.STATE_READY); verify(mockListener).onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE); assertThat(castPlayer.getPlayWhenReady()).isTrue(); + verify(mockListener).onIsPlayingChanged(true); + verifyNoMoreInteractions(mockListener); } @Test From a57245e7828a8bd465792c253e6a5d3cf6baee9b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 Dec 2021 18:04:00 +0000 Subject: [PATCH 51/56] Fix 1 ErrorProneStyle finding: * @Override is not a TYPE_USE annotation, so should appear before any modifiers and after Javadocs. @CryptoType is a TYPE_USE annotation, so should appear after modifiers and directly before the type. PiperOrigin-RevId: 418811744 --- .../com/google/android/exoplayer2/source/SampleQueueTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 037d245a65..afe4ce38fd 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -1758,8 +1758,7 @@ public final class SampleQueueTest { } @Override - @C.CryptoType - public int getCryptoType(Format format) { + public @C.CryptoType int getCryptoType(Format format) { return mockPlaceholderDrmSession != null || format.drmInitData != null ? FakeCryptoConfig.TYPE : C.CRYPTO_TYPE_NONE; From a9edb207a300d7b30c2e75c1011c3cd1c727b519 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 Dec 2021 18:24:52 +0000 Subject: [PATCH 52/56] Fix 1 ErrorProneStyle finding: * @CryptoType is a TYPE_USE annotation, so should appear after modifiers and directly before the type. PiperOrigin-RevId: 418814902 --- .../com/google/android/exoplayer2/ext/opus/OpusLibrary.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java index 01162bf365..3fa874ff91 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java @@ -28,7 +28,7 @@ public final class OpusLibrary { } private static final LibraryLoader LOADER = new LibraryLoader("opusV2JNI"); - @C.CryptoType private static int cryptoType = C.CRYPTO_TYPE_UNSUPPORTED; + private static @C.CryptoType int cryptoType = C.CRYPTO_TYPE_UNSUPPORTED; private OpusLibrary() {} From 47f4d90515eb447ea1e6955e9ecf7b75f75e355a Mon Sep 17 00:00:00 2001 From: hschlueter Date: Wed, 29 Dec 2021 19:10:12 +0000 Subject: [PATCH 53/56] Use TransformationException for GL errors. PiperOrigin-RevId: 418820557 --- .../FrameEditorDataProcessingTest.java | 236 +++++++++++++++++ .../transformer/FrameEditorTest.java | 238 +++--------------- .../exoplayer2/transformer/FrameEditor.java | 138 +++++----- .../transformer/TransformationException.java | 14 +- .../transformer/VideoSamplePipeline.java | 10 +- 5 files changed, 372 insertions(+), 264 deletions(-) create mode 100644 library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameEditorDataProcessingTest.java diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameEditorDataProcessingTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameEditorDataProcessingTest.java new file mode 100644 index 0000000000..a2762ed080 --- /dev/null +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameEditorDataProcessingTest.java @@ -0,0 +1,236 @@ +/* + * Copyright 2021 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 androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.common.truth.Truth.assertThat; +import static java.lang.Math.abs; +import static java.lang.Math.max; + +import android.content.res.AssetFileDescriptor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.PixelFormat; +import android.media.Image; +import android.media.ImageReader; +import android.media.MediaCodec; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.InputStream; +import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for frame processing via {@link FrameEditor#processData()}. */ +@RunWith(AndroidJUnit4.class) +public final class FrameEditorDataProcessingTest { + + private static final String INPUT_MP4_ASSET_STRING = "media/mp4/sample.mp4"; + private static final String NO_EDITS_EXPECTED_OUTPUT_PNG_ASSET_STRING = + "media/bitmap/sample_mp4_first_frame.png"; + /** + * Maximum allowed average pixel difference between the expected and actual edited images for the + * test to pass. The value is chosen so that differences in decoder behavior across emulator + * versions shouldn't affect whether the test passes, but substantial distortions introduced by + * changes in the behavior of the frame editor will cause the test to fail. + */ + private static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE = 0.1f; + /** Timeout for dequeueing buffers from the codec, in microseconds. */ + private static final int DEQUEUE_TIMEOUT_US = 5_000_000; + /** Time to wait for the frame editor's input to be populated by the decoder, in milliseconds. */ + private static final int SURFACE_WAIT_MS = 1000; + /** The ratio of width over height, for each pixel in a frame. */ + private static final float PIXEL_WIDTH_HEIGHT_RATIO = 1; + + private @MonotonicNonNull FrameEditor frameEditor; + private @MonotonicNonNull ImageReader frameEditorOutputImageReader; + private @MonotonicNonNull MediaFormat mediaFormat; + + @Before + public void setUp() throws Exception { + // Set up the extractor to read the first video frame and get its format. + MediaExtractor mediaExtractor = new MediaExtractor(); + @Nullable MediaCodec mediaCodec = null; + try (AssetFileDescriptor afd = + getApplicationContext().getAssets().openFd(INPUT_MP4_ASSET_STRING)) { + mediaExtractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); + for (int i = 0; i < mediaExtractor.getTrackCount(); i++) { + if (MimeTypes.isVideo(mediaExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME))) { + mediaFormat = mediaExtractor.getTrackFormat(i); + mediaExtractor.selectTrack(i); + break; + } + } + + int width = checkNotNull(mediaFormat).getInteger(MediaFormat.KEY_WIDTH); + int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT); + frameEditorOutputImageReader = + ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, /* maxImages= */ 1); + Matrix identityMatrix = new Matrix(); + frameEditor = + FrameEditor.create( + getApplicationContext(), + width, + height, + PIXEL_WIDTH_HEIGHT_RATIO, + identityMatrix, + frameEditorOutputImageReader.getSurface(), + Transformer.DebugViewProvider.NONE); + + // Queue the first video frame from the extractor. + String mimeType = checkNotNull(mediaFormat.getString(MediaFormat.KEY_MIME)); + mediaCodec = MediaCodec.createDecoderByType(mimeType); + mediaCodec.configure( + mediaFormat, frameEditor.getInputSurface(), /* crypto= */ null, /* flags= */ 0); + mediaCodec.start(); + int inputBufferIndex = mediaCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US); + assertThat(inputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + ByteBuffer inputBuffer = checkNotNull(mediaCodec.getInputBuffers()[inputBufferIndex]); + int sampleSize = mediaExtractor.readSampleData(inputBuffer, /* offset= */ 0); + mediaCodec.queueInputBuffer( + inputBufferIndex, + /* offset= */ 0, + sampleSize, + mediaExtractor.getSampleTime(), + mediaExtractor.getSampleFlags()); + + // Queue an end-of-stream buffer to force the codec to produce output. + inputBufferIndex = mediaCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US); + assertThat(inputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + mediaCodec.queueInputBuffer( + inputBufferIndex, + /* offset= */ 0, + /* size= */ 0, + /* presentationTimeUs= */ 0, + MediaCodec.BUFFER_FLAG_END_OF_STREAM); + + // Dequeue and render the output video frame. + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + int outputBufferIndex; + do { + outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIMEOUT_US); + assertThat(outputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } while (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED + || outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + mediaCodec.releaseOutputBuffer(outputBufferIndex, /* render= */ true); + + // Sleep to give time for the surface texture to be populated. + Thread.sleep(SURFACE_WAIT_MS); + assertThat(frameEditor.hasInputData()).isTrue(); + } finally { + mediaExtractor.release(); + if (mediaCodec != null) { + mediaCodec.release(); + } + } + } + + @After + public void tearDown() { + if (frameEditor != null) { + frameEditor.release(); + } + } + + @Test + public void processData_noEdits_producesExpectedOutput() throws Exception { + Bitmap expectedBitmap; + try (InputStream inputStream = + getApplicationContext().getAssets().open(NO_EDITS_EXPECTED_OUTPUT_PNG_ASSET_STRING)) { + expectedBitmap = BitmapFactory.decodeStream(inputStream); + } + + checkNotNull(frameEditor).processData(); + Image editedImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); + Bitmap editedBitmap = getArgb8888BitmapForRgba8888Image(editedImage); + + // TODO(internal b/207848601): switch to using proper tooling for testing against golden data. + float averagePixelAbsoluteDifference = + getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + /** + * Returns a bitmap with the same information as the provided alpha/red/green/blue 8-bits per + * component image. + */ + private static Bitmap getArgb8888BitmapForRgba8888Image(Image image) { + int width = image.getWidth(); + int height = image.getHeight(); + assertThat(image.getPlanes()).hasLength(1); + assertThat(image.getFormat()).isEqualTo(PixelFormat.RGBA_8888); + Image.Plane plane = image.getPlanes()[0]; + ByteBuffer buffer = plane.getBuffer(); + int[] colors = new int[width * height]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int offset = y * plane.getRowStride() + x * plane.getPixelStride(); + int r = buffer.get(offset) & 0xFF; + int g = buffer.get(offset + 1) & 0xFF; + int b = buffer.get(offset + 2) & 0xFF; + int a = buffer.get(offset + 3) & 0xFF; + colors[y * width + x] = (a << 24) + (r << 16) + (g << 8) + b; + } + } + return Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); + } + + /** + * Returns the sum of the absolute differences between the expected and actual bitmaps, calculated + * using the maximum difference across all color channels for each pixel, then divided by the + * total number of pixels in the image. The bitmap resolutions must match and they must use + * configuration {@link Bitmap.Config#ARGB_8888}. + */ + private static float getAveragePixelAbsoluteDifferenceArgb8888(Bitmap expected, Bitmap actual) { + int width = actual.getWidth(); + int height = actual.getHeight(); + assertThat(width).isEqualTo(expected.getWidth()); + assertThat(height).isEqualTo(expected.getHeight()); + assertThat(actual.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888); + long sumMaximumAbsoluteDifferences = 0; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int color = actual.getPixel(x, y); + int expectedColor = expected.getPixel(x, y); + int maximumAbsoluteDifference = 0; + maximumAbsoluteDifference = + max( + maximumAbsoluteDifference, + abs(((color >> 24) & 0xFF) - ((expectedColor >> 24) & 0xFF))); + maximumAbsoluteDifference = + max( + maximumAbsoluteDifference, + abs(((color >> 16) & 0xFF) - ((expectedColor >> 16) & 0xFF))); + maximumAbsoluteDifference = + max( + maximumAbsoluteDifference, + abs(((color >> 8) & 0xFF) - ((expectedColor >> 8) & 0xFF))); + maximumAbsoluteDifference = + max(maximumAbsoluteDifference, abs((color & 0xFF) - (expectedColor & 0xFF))); + sumMaximumAbsoluteDifferences += maximumAbsoluteDifference; + } + } + return (float) sumMaximumAbsoluteDifferences / (width * height); + } +} diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameEditorTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameEditorTest.java index b47205947f..a30ffc51c2 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameEditorTest.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameEditorTest.java @@ -16,221 +16,55 @@ package com.google.android.exoplayer2.transformer; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; -import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.common.truth.Truth.assertThat; -import static java.lang.Math.abs; -import static java.lang.Math.max; +import static org.junit.Assert.assertThrows; -import android.content.res.AssetFileDescriptor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; +import android.content.Context; import android.graphics.Matrix; -import android.graphics.PixelFormat; -import android.media.Image; -import android.media.ImageReader; -import android.media.MediaCodec; -import android.media.MediaExtractor; -import android.media.MediaFormat; -import androidx.annotation.Nullable; +import android.graphics.SurfaceTexture; +import android.view.Surface; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.util.MimeTypes; -import java.io.InputStream; -import java.nio.ByteBuffer; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.junit.After; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -/** Test for frame processing via {@link FrameEditor}. */ +/** + * Test for {@link FrameEditor#create(Context, int, int, float, Matrix, Surface, + * Transformer.DebugViewProvider) creating} a {@link FrameEditor}. + */ @RunWith(AndroidJUnit4.class) public final class FrameEditorTest { + // TODO(b/212539951): Make this a robolectric test by e.g. updating shadows or adding a + // wrapper around GlUtil to allow the usage of mocks or fakes which don't need (Shadow)GLES20. - private static final String INPUT_MP4_ASSET_STRING = "media/mp4/sample.mp4"; - private static final String NO_EDITS_EXPECTED_OUTPUT_PNG_ASSET_STRING = - "media/bitmap/sample_mp4_first_frame.png"; - /** - * Maximum allowed average pixel difference between the expected and actual edited images for the - * test to pass. The value is chosen so that differences in decoder behavior across emulator - * versions shouldn't affect whether the test passes, but substantial distortions introduced by - * changes in the behavior of the frame editor will cause the test to fail. - */ - private static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE = 0.1f; - /** Timeout for dequeueing buffers from the codec, in microseconds. */ - private static final int DEQUEUE_TIMEOUT_US = 5_000_000; - /** Time to wait for the frame editor's input to be populated by the decoder, in milliseconds. */ - private static final int SURFACE_WAIT_MS = 1000; - /** The ratio of width over height, for each pixel in a frame. */ - private static final float PIXEL_WIDTH_HEIGHT_RATIO = 1; - - private @MonotonicNonNull FrameEditor frameEditor; - private @MonotonicNonNull ImageReader frameEditorOutputImageReader; - private @MonotonicNonNull MediaFormat mediaFormat; - - @Before - public void setUp() throws Exception { - // Set up the extractor to read the first video frame and get its format. - MediaExtractor mediaExtractor = new MediaExtractor(); - @Nullable MediaCodec mediaCodec = null; - try (AssetFileDescriptor afd = - getApplicationContext().getAssets().openFd(INPUT_MP4_ASSET_STRING)) { - mediaExtractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); - for (int i = 0; i < mediaExtractor.getTrackCount(); i++) { - if (MimeTypes.isVideo(mediaExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME))) { - mediaFormat = mediaExtractor.getTrackFormat(i); - mediaExtractor.selectTrack(i); - break; - } - } - - int width = checkNotNull(mediaFormat).getInteger(MediaFormat.KEY_WIDTH); - int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT); - frameEditorOutputImageReader = - ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, /* maxImages= */ 1); - Matrix identityMatrix = new Matrix(); - frameEditor = - FrameEditor.create( - getApplicationContext(), - width, - height, - PIXEL_WIDTH_HEIGHT_RATIO, - identityMatrix, - frameEditorOutputImageReader.getSurface(), - Transformer.DebugViewProvider.NONE); - - // Queue the first video frame from the extractor. - String mimeType = checkNotNull(mediaFormat.getString(MediaFormat.KEY_MIME)); - mediaCodec = MediaCodec.createDecoderByType(mimeType); - mediaCodec.configure( - mediaFormat, frameEditor.getInputSurface(), /* crypto= */ null, /* flags= */ 0); - mediaCodec.start(); - int inputBufferIndex = mediaCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US); - assertThat(inputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - ByteBuffer inputBuffer = checkNotNull(mediaCodec.getInputBuffers()[inputBufferIndex]); - int sampleSize = mediaExtractor.readSampleData(inputBuffer, /* offset= */ 0); - mediaCodec.queueInputBuffer( - inputBufferIndex, - /* offset= */ 0, - sampleSize, - mediaExtractor.getSampleTime(), - mediaExtractor.getSampleFlags()); - - // Queue an end-of-stream buffer to force the codec to produce output. - inputBufferIndex = mediaCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US); - assertThat(inputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - mediaCodec.queueInputBuffer( - inputBufferIndex, - /* offset= */ 0, - /* size= */ 0, - /* presentationTimeUs= */ 0, - MediaCodec.BUFFER_FLAG_END_OF_STREAM); - - // Dequeue and render the output video frame. - MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - int outputBufferIndex; - do { - outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIMEOUT_US); - assertThat(outputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - } while (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED - || outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); - mediaCodec.releaseOutputBuffer(outputBufferIndex, /* render= */ true); - - // Sleep to give time for the surface texture to be populated. - Thread.sleep(SURFACE_WAIT_MS); - assertThat(frameEditor.hasInputData()).isTrue(); - } finally { - mediaExtractor.release(); - if (mediaCodec != null) { - mediaCodec.release(); - } - } - } - - @After - public void tearDown() { - if (frameEditor != null) { - frameEditor.release(); - } + @Test + public void create_withSupportedPixelWidthHeightRatio_completesSuccessfully() + throws TransformationException { + FrameEditor.create( + getApplicationContext(), + /* outputWidth= */ 200, + /* outputHeight= */ 100, + /* pixelWidthHeightRatio= */ 1, + new Matrix(), + new Surface(new SurfaceTexture(false)), + Transformer.DebugViewProvider.NONE); } @Test - public void processData_noEdits_producesExpectedOutput() throws Exception { - Bitmap expectedBitmap; - try (InputStream inputStream = - getApplicationContext().getAssets().open(NO_EDITS_EXPECTED_OUTPUT_PNG_ASSET_STRING)) { - expectedBitmap = BitmapFactory.decodeStream(inputStream); - } + public void create_withUnsupportedPixelWidthHeightRatio_throwsException() { + TransformationException exception = + assertThrows( + TransformationException.class, + () -> + FrameEditor.create( + getApplicationContext(), + /* outputWidth= */ 200, + /* outputHeight= */ 100, + /* pixelWidthHeightRatio= */ 2, + new Matrix(), + new Surface(new SurfaceTexture(false)), + Transformer.DebugViewProvider.NONE)); - checkNotNull(frameEditor).processData(); - Image editedImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); - Bitmap editedBitmap = getArgb8888BitmapForRgba8888Image(editedImage); - - // TODO(internal b/207848601): switch to using proper tooling for testing against golden data. - float averagePixelAbsoluteDifference = - getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); - assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); - } - - /** - * Returns a bitmap with the same information as the provided alpha/red/green/blue 8-bits per - * component image. - */ - private static Bitmap getArgb8888BitmapForRgba8888Image(Image image) { - int width = image.getWidth(); - int height = image.getHeight(); - assertThat(image.getPlanes()).hasLength(1); - assertThat(image.getFormat()).isEqualTo(PixelFormat.RGBA_8888); - Image.Plane plane = image.getPlanes()[0]; - ByteBuffer buffer = plane.getBuffer(); - int[] colors = new int[width * height]; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int offset = y * plane.getRowStride() + x * plane.getPixelStride(); - int r = buffer.get(offset) & 0xFF; - int g = buffer.get(offset + 1) & 0xFF; - int b = buffer.get(offset + 2) & 0xFF; - int a = buffer.get(offset + 3) & 0xFF; - colors[y * width + x] = (a << 24) + (r << 16) + (g << 8) + b; - } - } - return Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); - } - - /** - * Returns the sum of the absolute differences between the expected and actual bitmaps, calculated - * using the maximum difference across all color channels for each pixel, then divided by the - * total number of pixels in the image. The bitmap resolutions must match and they must use - * configuration {@link Bitmap.Config#ARGB_8888}. - */ - private static float getAveragePixelAbsoluteDifferenceArgb8888(Bitmap expected, Bitmap actual) { - int width = actual.getWidth(); - int height = actual.getHeight(); - assertThat(width).isEqualTo(expected.getWidth()); - assertThat(height).isEqualTo(expected.getHeight()); - assertThat(actual.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888); - long sumMaximumAbsoluteDifferences = 0; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int color = actual.getPixel(x, y); - int expectedColor = expected.getPixel(x, y); - int maximumAbsoluteDifference = 0; - maximumAbsoluteDifference = - max( - maximumAbsoluteDifference, - abs(((color >> 24) & 0xFF) - ((expectedColor >> 24) & 0xFF))); - maximumAbsoluteDifference = - max( - maximumAbsoluteDifference, - abs(((color >> 16) & 0xFF) - ((expectedColor >> 16) & 0xFF))); - maximumAbsoluteDifference = - max( - maximumAbsoluteDifference, - abs(((color >> 8) & 0xFF) - ((expectedColor >> 8) & 0xFF))); - maximumAbsoluteDifference = - max(maximumAbsoluteDifference, abs((color & 0xFF) - (expectedColor & 0xFF))); - sumMaximumAbsoluteDifferences += maximumAbsoluteDifference; - } - } - return (float) sumMaximumAbsoluteDifferences / (width * height); + assertThat(exception).hasCauseThat().isInstanceOf(UnsupportedOperationException.class); + assertThat(exception).hasCauseThat().hasMessageThat().contains("pixelWidthHeightRatio"); } } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java index 1846cd956e..29e0d50a8e 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameEditor.java @@ -52,6 +52,9 @@ import java.util.concurrent.atomic.AtomicInteger; * @param outputSurface The {@link Surface}. * @param debugViewProvider Provider for optional debug views to show intermediate output. * @return A configured {@code FrameEditor}. + * @throws TransformationException If the {@code pixelWidthHeightRatio} isn't 1, reading shader + * files fails, or an OpenGL error occurs while creating and configuring the OpenGL + * components. */ public static FrameEditor create( Context context, @@ -63,30 +66,71 @@ import java.util.concurrent.atomic.AtomicInteger; Transformer.DebugViewProvider debugViewProvider) throws TransformationException { if (pixelWidthHeightRatio != 1.0f) { - // TODO(http://b/211782176): Consider implementing support for non-square pixels. - throw new TransformationException( - "FrameEditor Error", - new IllegalArgumentException( + // TODO(b/211782176): Consider implementing support for non-square pixels. + throw TransformationException.createForFrameEditor( + new UnsupportedOperationException( "Transformer's frame editor currently does not support frame edits on non-square" + " pixels. The pixelWidthHeightRatio is: " + pixelWidthHeightRatio), TransformationException.ERROR_CODE_GL_INIT_FAILED); } - EGLDisplay eglDisplay = GlUtil.createEglDisplay(); - EGLContext eglContext = GlUtil.createEglContext(eglDisplay); - EGLSurface eglSurface = GlUtil.getEglSurface(eglDisplay, outputSurface); - GlUtil.focusSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); - int textureId = GlUtil.createExternalTexture(); + @Nullable + SurfaceView debugSurfaceView = + debugViewProvider.getDebugPreviewSurfaceView(outputWidth, outputHeight); + + EGLDisplay eglDisplay; + EGLContext eglContext; + EGLSurface eglSurface; + int textureId; GlUtil.Program glProgram; + @Nullable EGLSurface debugPreviewEglSurface; try { - // TODO(internal b/205002913): check the loaded program is consistent with the attributes - // and uniforms expected in the code. - glProgram = new GlUtil.Program(context, VERTEX_SHADER_FILE_PATH, FRAGMENT_SHADER_FILE_PATH); - } catch (IOException e) { - throw new IllegalStateException(e); + eglDisplay = GlUtil.createEglDisplay(); + eglContext = GlUtil.createEglContext(eglDisplay); + eglSurface = GlUtil.getEglSurface(eglDisplay, outputSurface); + GlUtil.focusSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); + textureId = GlUtil.createExternalTexture(); + glProgram = configureGlProgram(context, transformationMatrix, textureId); + debugPreviewEglSurface = + debugSurfaceView == null + ? null + : GlUtil.getEglSurface(eglDisplay, checkNotNull(debugSurfaceView.getHolder())); + } catch (IOException | GlUtil.GlException e) { + throw TransformationException.createForFrameEditor( + e, TransformationException.ERROR_CODE_GL_INIT_FAILED); } + int debugPreviewWidth; + int debugPreviewHeight; + if (debugSurfaceView != null) { + debugPreviewWidth = debugSurfaceView.getWidth(); + debugPreviewHeight = debugSurfaceView.getHeight(); + } else { + debugPreviewWidth = C.LENGTH_UNSET; + debugPreviewHeight = C.LENGTH_UNSET; + } + + return new FrameEditor( + eglDisplay, + eglContext, + eglSurface, + textureId, + glProgram, + outputWidth, + outputHeight, + debugPreviewEglSurface, + debugPreviewWidth, + debugPreviewHeight); + } + + private static GlUtil.Program configureGlProgram( + Context context, Matrix transformationMatrix, int textureId) throws IOException { + // TODO(b/205002913): check the loaded program is consistent with the attributes + // and uniforms expected in the code. + GlUtil.Program glProgram = + new GlUtil.Program(context, VERTEX_SHADER_FILE_PATH, FRAGMENT_SHADER_FILE_PATH); + glProgram.setBufferAttribute( "aPosition", new float[] { @@ -109,34 +153,7 @@ import java.util.concurrent.atomic.AtomicInteger; float[] transformationMatrixArray = getGlMatrixArray(transformationMatrix); glProgram.setFloatsUniform("uTransformationMatrix", transformationMatrixArray); - - @Nullable - SurfaceView debugSurfaceView = - debugViewProvider.getDebugPreviewSurfaceView(outputWidth, outputHeight); - @Nullable EGLSurface debugPreviewEglSurface; - int debugPreviewWidth; - int debugPreviewHeight; - if (debugSurfaceView != null) { - debugPreviewEglSurface = - GlUtil.getEglSurface(eglDisplay, checkNotNull(debugSurfaceView.getHolder())); - debugPreviewWidth = debugSurfaceView.getWidth(); - debugPreviewHeight = debugSurfaceView.getHeight(); - } else { - debugPreviewEglSurface = null; - debugPreviewWidth = C.LENGTH_UNSET; - debugPreviewHeight = C.LENGTH_UNSET; - } - return new FrameEditor( - eglDisplay, - eglContext, - eglSurface, - textureId, - glProgram, - outputWidth, - outputHeight, - debugPreviewEglSurface, - debugPreviewWidth, - debugPreviewHeight); + return glProgram; } /** @@ -240,22 +257,31 @@ import java.util.concurrent.atomic.AtomicInteger; return pendingInputFrameCount.get() > 0; } - /** Processes pending input frame. */ - public void processData() { - inputSurfaceTexture.updateTexImage(); - inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); - glProgram.setFloatsUniform("uTexTransform", textureTransformMatrix); - glProgram.bindAttributesAndUniforms(); + /** + * Processes pending input frame. + * + * @throws TransformationException If an OpenGL error occurs while processing the data. + */ + public void processData() throws TransformationException { + try { + inputSurfaceTexture.updateTexImage(); + inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); + glProgram.setFloatsUniform("uTexTransform", textureTransformMatrix); + glProgram.bindAttributesAndUniforms(); - focusAndDrawQuad(eglSurface, outputWidth, outputHeight); - long surfaceTextureTimestampNs = inputSurfaceTexture.getTimestamp(); - EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, surfaceTextureTimestampNs); - EGL14.eglSwapBuffers(eglDisplay, eglSurface); - pendingInputFrameCount.decrementAndGet(); + focusAndDrawQuad(eglSurface, outputWidth, outputHeight); + long surfaceTextureTimestampNs = inputSurfaceTexture.getTimestamp(); + EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, surfaceTextureTimestampNs); + EGL14.eglSwapBuffers(eglDisplay, eglSurface); + pendingInputFrameCount.decrementAndGet(); - if (debugPreviewEglSurface != null) { - focusAndDrawQuad(debugPreviewEglSurface, debugPreviewWidth, debugPreviewHeight); - EGL14.eglSwapBuffers(eglDisplay, debugPreviewEglSurface); + if (debugPreviewEglSurface != null) { + focusAndDrawQuad(debugPreviewEglSurface, debugPreviewWidth, debugPreviewHeight); + EGL14.eglSwapBuffers(eglDisplay, debugPreviewEglSurface); + } + } catch (GlUtil.GlException e) { + throw TransformationException.createForFrameEditor( + e, TransformationException.ERROR_CODE_GL_PROCESSING_FAILED); } } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java index 0e56881c72..c5a4107b43 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformationException.java @@ -229,7 +229,7 @@ public final class TransformationException extends Exception { } /** - * Creates an instance for an audio processing related exception. + * Creates an instance for an {@link AudioProcessor} related exception. * * @param cause The cause of the failure. * @param componentName The name of the {@link AudioProcessor} used. @@ -243,6 +243,18 @@ public final class TransformationException extends Exception { componentName + " error, audio_format = " + audioFormat, cause, errorCode); } + /** + * Creates an instance for a {@link FrameEditor} related exception. + * + * @param cause The cause of the failure. + * @param errorCode See {@link #errorCode}. + * @return The created instance. + */ + /* package */ static TransformationException createForFrameEditor( + Throwable cause, int errorCode) { + return new TransformationException("FrameEditor error", cause, errorCode); + } + /** * Creates an instance for a muxer related exception. * diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java index 74f9fb237a..0da5b88b7d 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java @@ -81,7 +81,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } // The decoder rotates videos to their intended display orientation. The frameEditor rotates // them back for improved encoder compatibility. - // TODO(internal b/201293185): After fragment shader transformations are implemented, put + // TODO(b/201293185): After fragment shader transformations are implemented, put // postrotation in a later vertex shader. transformationRequest.transformationMatrix.postRotate(outputRotationDegrees); @@ -129,7 +129,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @Override - public boolean processData() { + public boolean processData() throws TransformationException { if (decoder.isEnded()) { return false; } @@ -155,7 +155,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * Transformer}, using this method requires API level 29 or higher. */ @RequiresApi(29) - private boolean processDataV29() { + private boolean processDataV29() throws TransformationException { if (frameEditor != null) { while (frameEditor.hasInputData()) { // Processes as much frames in one invocation: FrameEditor's output surface will block @@ -170,7 +170,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } if (decoder.isEnded()) { - // TODO(internal b/208986865): Handle possible last frame drop. + // TODO(b/208986865): Handle possible last frame drop. encoder.signalEndOfInputStream(); return false; } @@ -179,7 +179,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } /** Processes input data. */ - private boolean processDataDefault() { + private boolean processDataDefault() throws TransformationException { if (frameEditor != null) { if (frameEditor.hasInputData()) { waitingForFrameEditorInput = false; From 5370332fcc23a6a88f0d7b1a76dc6a791228f7f8 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 30 Dec 2021 12:20:37 +0000 Subject: [PATCH 54/56] DASH: Stop interpreting `main` track role as `SELECTION_FLAG_DEFAULT` The `main` role distinguishes a track from an `alternate`, but unlike `SELECTION_FLAG_DEFAULT` it doesn't imply the track should be selected unless user preferences state otherwise. e.g. in the case of a text track, the player shouldn't enable subtitle rendering just because a `main` text track is present in the manifest. The `main`/`alternate` distinction is still available through `Format.roleFlags` and the `ROLE_FLAG_MAIN` and `ROLE_FLAG_ALTERNATE` values. This behaviour was originally [added in 2.2.0](https://github.com/google/ExoPlayer/commit/7f967f305718bc2c9ee679fdd7d014eccef0356b), however at the time the `C.RoleFlags` IntDef did not exist. The IntDef was [added in 2.10.0](https://github.com/google/ExoPlayer/commit/a86a9137be5f0ed89de3d68f4c4800a7753cc881). PiperOrigin-RevId: 418937747 --- RELEASENOTES.md | 1 + .../exoplayer2/source/dash/manifest/DashManifestParser.java | 2 -- .../source/dash/manifest/DashManifestParserTest.java | 3 ++- testdata/src/test/assets/media/mpd/sample_mpd_text | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7c1e42c991..9e02140724 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -59,6 +59,7 @@ * DASH: * Support the `forced-subtitle` track role ([#9727](https://github.com/google/ExoPlayer/issues/9727)). + * Stop interpreting the `main` track role as `C.SELECTION_FLAG_DEFAULT`. * HLS: * Use chunkless preparation by default to improve start up time. If your renditions contain muxed closed-caption tracks that are *not* declared diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 4ade8ce361..e0d7e2c43a 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -1472,8 +1472,6 @@ public class DashManifestParser extends DefaultHandler return 0; } switch (value) { - case "main": - return C.SELECTION_FLAG_DEFAULT; case "forced_subtitle": // Support both hyphen and underscore (https://github.com/google/ExoPlayer/issues/9727). case "forced-subtitle": diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index cb2d216c32..6516f9d71a 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -243,7 +243,8 @@ public class DashManifestParserTest { assertThat(format.containerMimeType).isEqualTo(MimeTypes.APPLICATION_RAWCC); assertThat(format.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA608); assertThat(format.codecs).isEqualTo("cea608"); - assertThat(format.roleFlags).isEqualTo(C.ROLE_FLAG_SUBTITLE); + assertThat(format.roleFlags).isEqualTo(C.ROLE_FLAG_SUBTITLE | C.ROLE_FLAG_MAIN); + assertThat(format.selectionFlags).isEqualTo(0); assertThat(adaptationSets.get(0).type).isEqualTo(C.TRACK_TYPE_TEXT); format = adaptationSets.get(1).representations.get(0).format; diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_text b/testdata/src/test/assets/media/mpd/sample_mpd_text index 4220f5b2f2..9af9e24915 100644 --- a/testdata/src/test/assets/media/mpd/sample_mpd_text +++ b/testdata/src/test/assets/media/mpd/sample_mpd_text @@ -8,6 +8,7 @@ + https://test.com/0 From 1380655747b7364f06774dacd2dda5b1d577b812 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Dec 2021 16:10:35 +0000 Subject: [PATCH 55/56] Parse CryptoInfo from simpleTag and set it into DrmInitData. PiperOrigin-RevId: 418960700 --- .../exoplayer2/extractor/mkv/MatroskaExtractor.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 3a0091e28a..e8876ca52c 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -1325,7 +1325,12 @@ public class MatroskaExtractor implements Extractor { } } - private Track getCurrentTrack(int currentElementId) throws ParserException { + /** + * Returns the track corresponding to the current TrackEntry element. + * + * @throws ParserException if the element id is not in a TrackEntry. + */ + protected Track getCurrentTrack(int currentElementId) throws ParserException { assertInTrackEntry(currentElementId); return currentTrack; } @@ -1900,7 +1905,8 @@ public class MatroskaExtractor implements Extractor { } } - private static final class Track { + /** Holds data corresponding to a single track. */ + protected static final class Track { private static final int DISPLAY_UNIT_PIXELS = 0; private static final int MAX_CHROMATICITY = 50_000; // Defined in CTA-861.3. From 1852c79ba4f69ce817663cbe7f86ea73a433ce38 Mon Sep 17 00:00:00 2001 From: John BoWeRs Date: Tue, 4 Jan 2022 12:43:14 -0700 Subject: [PATCH 56/56] Add support for uppercase hex template number formatting as well as lowercase --- .../android/exoplayer2/source/dash/manifest/UrlTemplate.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java index cc3597a33a..a70fdb855c 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java @@ -140,7 +140,7 @@ public final class UrlTemplate { // Allowed conversions are decimal integer (which is the only conversion allowed by the // DASH specification) and hexadecimal integer (due to existing content that uses it). // Else we assume that the conversion is missing, and that it should be decimal integer. - if (!formatTag.endsWith("d") && !formatTag.endsWith("x")) { + if (!formatTag.endsWith("d") && !formatTag.endsWith("x") && !formatTag.endsWith("X")) { formatTag += "d"; } identifier = identifier.substring(0, formatTagIndex);