diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e080c7bc2b..39472622ea 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,7 +2,12 @@ ### dev-v2 (not yet released) +### 2.16.1 (2021-11-11) + * Core Library: + * Fix track selection issue where overriding one track group did not + disable other track groups of the same type + ([#9675](https://github.com/google/ExoPlayer/issues/9675)). * Fix track selection issue where a mixture of non-empty and empty track overrides is not applied correctly ([#9649](https://github.com/google/ExoPlayer/issues/9649). @@ -14,6 +19,9 @@ * Extractors: * WAV: Add support for RF64 streams ([#9543](https://github.com/google/ExoPlayer/issues/9543). +* DASH: + * Add parsed essential and supplemental properties to the `Representation` + ([#9579](https://github.com/google/ExoPlayer/issues/9579)). * RTSP * Provide a client API to override the `SocketFactory` used for any server connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)). diff --git a/constants.gradle b/constants.gradle index bd4a545c4c..7fd89a86d6 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.16.0' - releaseVersionCode = 2016000 + releaseVersion = '2.16.1' + releaseVersionCode = 2016001 minSdkVersion = 16 appTargetSdkVersion = 29 // Upgrading this requires [Internal ref: b/193254928] to be fixed, or some @@ -26,33 +26,32 @@ project.ext { // Use the same Guava version as the Android repo: // https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA guavaVersion = '27.1-android' - mockitoVersion = '3.4.0' - mockWebServerVersion = '3.12.0' + mockitoVersion = '3.12.4' robolectricVersion = '4.6.1' // Keep this in sync with Google's internal Checker Framework version. - checkerframeworkVersion = '3.5.0' - checkerframeworkCompatVersion = '2.5.0' - errorProneVersion = '2.9.0' + checkerframeworkVersion = '3.13.0' + checkerframeworkCompatVersion = '2.5.5' + errorProneVersion = '2.10.0' jsr305Version = '3.0.2' - kotlinAnnotationsVersion = '1.5.20' - androidxAnnotationVersion = '1.2.0' - androidxAppCompatVersion = '1.3.0' + kotlinAnnotationsVersion = '1.5.31' + androidxAnnotationVersion = '1.3.0' + androidxAppCompatVersion = '1.3.1' androidxCollectionVersion = '1.1.0' - androidxCoreVersion = '1.6.0' + androidxCoreVersion = '1.7.0' androidxFuturesVersion = '1.1.0' androidxMediaVersion = '1.4.3' - androidxMedia2Version = '1.1.3' + androidxMedia2Version = '1.2.0' androidxMultidexVersion = '2.0.1' androidxRecyclerViewVersion = '1.2.1' - androidxMaterialVersion = '1.3.0' - androidxTestCoreVersion = '1.3.0' - androidxTestJUnitVersion = '1.1.2' - androidxTestRunnerVersion = '1.3.0' - androidxTestRulesVersion = '1.3.0' - androidxTestServicesStorageVersion = '1.3.0' - androidxTestTruthVersion = '1.3.0' + androidxMaterialVersion = '1.4.0' + androidxTestCoreVersion = '1.4.0' + androidxTestJUnitVersion = '1.1.3' + androidxTestRunnerVersion = '1.4.0' + androidxTestRulesVersion = '1.4.0' + androidxTestServicesStorageVersion = '1.4.0' + androidxTestTruthVersion = '1.4.0' truthVersion = '1.1.3' - okhttpVersion = '4.9.1' + okhttpVersion = '4.9.2' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/docs/hello-world.md b/docs/hello-world.md index 9ad62fb617..5b71f150ff 100644 --- a/docs/hello-world.md +++ b/docs/hello-world.md @@ -126,7 +126,7 @@ may result in unexpected or obscure errors. It will be removed in ExoPlayer 2.16. {:.info} -For more information about ExoPlayer's treading model, see the +For more information about ExoPlayer's threading model, see the ["Threading model" section of the ExoPlayer Javadoc][]. ## Attaching the player to a view ## diff --git a/docs/listening-to-player-events.md b/docs/listening-to-player-events.md index 99af0b6678..3de8bde96b 100644 --- a/docs/listening-to-player-events.md +++ b/docs/listening-to-player-events.md @@ -31,7 +31,8 @@ Changes in player state can be received by implementing `Player.Listener`. The player can be in one of four playback states: * `Player.STATE_IDLE`: This is the initial state, the state when the player is - stopped, and when playback failed. + stopped, and when playback failed. The player will hold only limited resources + in this state. * `Player.STATE_BUFFERING`: The player is not able to immediately play from its current position. This mostly happens because more data needs to be loaded. * `Player.STATE_READY`: The player is able to immediately play from its current 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 516a6fdde4..917d25aba0 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 @@ -392,7 +392,6 @@ public class CastPlayerTest { assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(uri2); } - @SuppressWarnings("deprecation") // Verifies deprecated callback being called correctly. @Test public void setMediaItems_replaceExistingPlaylist_notifiesMediaItemTransition() { List firstPlaylist = new ArrayList<>(); diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 6fdba05663..09211ca4cb 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -32,7 +32,7 @@ dependencies { androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion // Instrumentation tests assume that an app-packaged version of cronet is // available. - androidTestImplementation 'org.chromium.net:cronet-embedded:94.4606.61' + androidTestImplementation 'org.chromium.net:cronet-embedded:95.4638.50' androidTestImplementation project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils') testImplementation 'com.squareup.okhttp3:mockwebserver:' + okhttpVersion diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 26e87025e1..4a40f0afc9 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -25,7 +25,7 @@ android { } dependencies { - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.24.0' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.25.1' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index bd70d14394..61c3673e5e 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -134,8 +134,6 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab return player.getPlaybackState() == Player.STATE_IDLE ? -1 : player.getCurrentPosition(); } - // Calls deprecated method to provide backwards compatibility. - @SuppressWarnings("deprecation") @Override public void play() { if (player.getPlaybackState() == Player.STATE_IDLE) { diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 4784575659..60b170bb4c 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -27,11 +27,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.16.0"; + public static final String VERSION = "2.16.1"; /** The version of the library expressed as {@code TAG + "/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.16.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.16.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -41,7 +41,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2016000; + public static final int VERSION_INT = 2016001; /** Whether the library was compiled with {@link Assertions} checks enabled. */ public static final boolean ASSERTIONS_ENABLED = true; diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Player.java b/library/common/src/main/java/com/google/android/exoplayer2/Player.java index 406398e39f..28aa61057d 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Player.java @@ -1087,7 +1087,10 @@ public interface Player { @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) @IntDef({STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED}) @interface State {} - /** The player is idle, and must be {@link #prepare() prepared} before it will play the media. */ + /** + * The player is idle, meaning it holds only limited resources. The player must be {@link + * #prepare() prepared} before it will play the media. + */ int STATE_IDLE = 1; /** * The player is not able to immediately play the media, but is doing work toward being able to do @@ -1669,7 +1672,12 @@ public interface Player { */ Commands getAvailableCommands(); - /** Prepares the player. */ + /** + * Prepares the player. + * + *

This will move the player out of {@link #STATE_IDLE idle state} and the player will start + * loading media and acquire resources needed for playback. + */ void prepare(); /** @@ -2001,12 +2009,13 @@ public interface Player { PlaybackParameters getPlaybackParameters(); /** - * Stops playback without resetting the player. Use {@link #pause()} rather than this method if + * Stops playback without resetting the playlist. Use {@link #pause()} rather than this method if * the intention is to pause playback. * - *

Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The - * player instance can still be used, and {@link #release()} must still be called on the player if - * it's no longer required. + *

Calling this method will cause the playback state to transition to {@link #STATE_IDLE} and + * the player will release the loaded media and resources required for playback. The player + * instance can still be used by calling {@link #prepare()} again, and {@link #release()} must + * still be called on the player if it's no longer required. * *

Calling this method does not clear the playlist, reset the playback position or the playback * error. diff --git a/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java index a386580f3d..436bea7048 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -362,7 +362,6 @@ public final class Cue implements Bundleable { * @param textSize See {@link #textSize}. * @deprecated Use {@link Builder}. */ - @SuppressWarnings("deprecation") @Deprecated public Cue( CharSequence text, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index d2ea709e76..65c00a8a59 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -262,7 +262,6 @@ public class DefaultLoadControl implements LoadControl { private boolean isLoading; /** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */ - @SuppressWarnings("deprecation") public DefaultLoadControl() { this( new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE), 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 83067a500c..fdc819078b 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 @@ -396,7 +396,6 @@ public class SimpleExoPlayer extends BasePlayer /** @deprecated Use the {@link ExoPlayer.Builder}. */ @Deprecated - @SuppressWarnings("deprecation") protected SimpleExoPlayer( Context context, RenderersFactory renderersFactory, @@ -428,7 +427,6 @@ public class SimpleExoPlayer extends BasePlayer } /** @param builder The {@link ExoPlayer.Builder} to obtain all construction parameters. */ - @SuppressWarnings("deprecation") /* package */ SimpleExoPlayer(ExoPlayer.Builder builder) { constructorFinished = new ConditionVariable(); try { 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 d997187a9d..ebafbee31d 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 @@ -753,6 +753,8 @@ public class DashManifestParser extends DefaultHandler drmSchemeType, drmSchemeDatas, inbandEventStreams, + essentialProperties, + supplementalProperties, Representation.REVISION_ID_DEFAULT); } @@ -841,7 +843,10 @@ public class DashManifestParser extends DefaultHandler formatBuilder.build(), representationInfo.baseUrls, representationInfo.segmentBase, - inbandEventStreams); + inbandEventStreams, + representationInfo.essentialProperties, + representationInfo.supplementalProperties, + /* cacheKey= */ null); } // SegmentBase, SegmentList and SegmentTemplate parsing. @@ -1910,6 +1915,8 @@ public class DashManifestParser extends DefaultHandler public final ArrayList drmSchemeDatas; public final ArrayList inbandEventStreams; public final long revisionId; + public final List essentialProperties; + public final List supplementalProperties; public RepresentationInfo( Format format, @@ -1918,6 +1925,8 @@ public class DashManifestParser extends DefaultHandler @Nullable String drmSchemeType, ArrayList drmSchemeDatas, ArrayList inbandEventStreams, + List essentialProperties, + List supplementalProperties, long revisionId) { this.format = format; this.baseUrls = ImmutableList.copyOf(baseUrls); @@ -1925,6 +1934,8 @@ public class DashManifestParser extends DefaultHandler this.drmSchemeType = drmSchemeType; this.drmSchemeDatas = drmSchemeDatas; this.inbandEventStreams = inbandEventStreams; + this.essentialProperties = essentialProperties; + this.supplementalProperties = supplementalProperties; this.revisionId = revisionId; } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index af9771ef14..1e02e9cd62 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -50,6 +50,10 @@ public abstract class Representation { public final long presentationTimeOffsetUs; /** The in-band event streams in the representation. May be empty. */ public final List inbandEventStreams; + /** Essential properties in the representation. May be empty. */ + public final List essentialProperties; + /** Supplemental properties in the adaptation set. May be empty. */ + public final List supplementalProperties; private final RangedUri initializationUri; @@ -64,27 +68,15 @@ public abstract class Representation { */ public static Representation newInstance( long revisionId, Format format, List baseUrls, SegmentBase segmentBase) { - return newInstance(revisionId, format, baseUrls, segmentBase, /* inbandEventStreams= */ null); - } - - /** - * Constructs a new instance. - * - * @param revisionId Identifies the revision of the content. - * @param format The format of the representation. - * @param baseUrls The list of base URLs of the representation. - * @param segmentBase A segment base element for the representation. - * @param inbandEventStreams The in-band event streams in the representation. May be null. - * @return The constructed instance. - */ - public static Representation newInstance( - long revisionId, - Format format, - List baseUrls, - SegmentBase segmentBase, - @Nullable List inbandEventStreams) { return newInstance( - revisionId, format, baseUrls, segmentBase, inbandEventStreams, /* cacheKey= */ null); + revisionId, + format, + baseUrls, + segmentBase, + /* inbandEventStreams= */ null, + /* essentialProperties= */ ImmutableList.of(), + /* supplementalProperties= */ ImmutableList.of(), + /* cacheKey= */ null); } /** @@ -95,6 +87,8 @@ public abstract class Representation { * @param baseUrls The list of base URLs of the representation. * @param segmentBase A segment base element for the representation. * @param inbandEventStreams The in-band event streams in the representation. May be null. + * @param essentialProperties Essential properties in the representation. May be empty. + * @param supplementalProperties Supplemental properties in the representation. May be empty. * @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null. This * parameter is ignored if {@code segmentBase} consists of multiple segments. * @return The constructed instance. @@ -105,6 +99,8 @@ public abstract class Representation { List baseUrls, SegmentBase segmentBase, @Nullable List inbandEventStreams, + List essentialProperties, + List supplementalProperties, @Nullable String cacheKey) { if (segmentBase instanceof SingleSegmentBase) { return new SingleSegmentRepresentation( @@ -113,11 +109,19 @@ public abstract class Representation { baseUrls, (SingleSegmentBase) segmentBase, inbandEventStreams, + essentialProperties, + supplementalProperties, cacheKey, - C.LENGTH_UNSET); + /* contentLength= */ C.LENGTH_UNSET); } else if (segmentBase instanceof MultiSegmentBase) { return new MultiSegmentRepresentation( - revisionId, format, baseUrls, (MultiSegmentBase) segmentBase, inbandEventStreams); + revisionId, + format, + baseUrls, + (MultiSegmentBase) segmentBase, + inbandEventStreams, + essentialProperties, + supplementalProperties); } else { throw new IllegalArgumentException( "segmentBase must be of type SingleSegmentBase or " + "MultiSegmentBase"); @@ -129,7 +133,9 @@ public abstract class Representation { Format format, List baseUrls, SegmentBase segmentBase, - @Nullable List inbandEventStreams) { + @Nullable List inbandEventStreams, + List essentialProperties, + List supplementalProperties) { checkArgument(!baseUrls.isEmpty()); this.revisionId = revisionId; this.format = format; @@ -138,6 +144,8 @@ public abstract class Representation { inbandEventStreams == null ? Collections.emptyList() : Collections.unmodifiableList(inbandEventStreams); + this.essentialProperties = essentialProperties; + this.supplementalProperties = supplementalProperties; initializationUri = segmentBase.getInitialization(this); presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs(); } @@ -207,7 +215,15 @@ public abstract class Representation { new SingleSegmentBase(rangedUri, 1, 0, indexStart, indexEnd - indexStart + 1); List baseUrls = ImmutableList.of(new BaseUrl(uri)); return new SingleSegmentRepresentation( - revisionId, format, baseUrls, segmentBase, inbandEventStreams, cacheKey, contentLength); + revisionId, + format, + baseUrls, + segmentBase, + inbandEventStreams, + /* essentialProperties= */ ImmutableList.of(), + /* supplementalProperties= */ ImmutableList.of(), + cacheKey, + contentLength); } /** @@ -216,6 +232,8 @@ public abstract class Representation { * @param baseUrls The base urls of the representation. * @param segmentBase The segment base underlying the representation. * @param inbandEventStreams The in-band event streams in the representation. May be null. + * @param essentialProperties Essential properties in the representation. May be empty. + * @param supplementalProperties Supplemental properties in the representation. May be empty. * @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null. * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown. */ @@ -225,9 +243,18 @@ public abstract class Representation { List baseUrls, SingleSegmentBase segmentBase, @Nullable List inbandEventStreams, + List essentialProperties, + List supplementalProperties, @Nullable String cacheKey, long contentLength) { - super(revisionId, format, baseUrls, segmentBase, inbandEventStreams); + super( + revisionId, + format, + baseUrls, + segmentBase, + inbandEventStreams, + essentialProperties, + supplementalProperties); this.uri = Uri.parse(baseUrls.get(0).url); this.indexUri = segmentBase.getIndex(); this.cacheKey = cacheKey; @@ -271,14 +298,25 @@ public abstract class Representation { * @param baseUrls The base URLs of the representation. * @param segmentBase The segment base underlying the representation. * @param inbandEventStreams The in-band event streams in the representation. May be null. + * @param essentialProperties Essential properties in the representation. May be empty. + * @param supplementalProperties Supplemental properties in the representation. May be empty. */ public MultiSegmentRepresentation( long revisionId, Format format, List baseUrls, MultiSegmentBase segmentBase, - @Nullable List inbandEventStreams) { - super(revisionId, format, baseUrls, segmentBase, inbandEventStreams); + @Nullable List inbandEventStreams, + List essentialProperties, + List supplementalProperties) { + super( + revisionId, + format, + baseUrls, + segmentBase, + inbandEventStreams, + essentialProperties, + supplementalProperties); this.segmentBase = segmentBase; } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java index d0cb9dabdd..664ea0995d 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java @@ -79,6 +79,8 @@ public final class DashUtilTest { baseUrls, new SingleSegmentBase(), /* inbandEventStreams= */ null, + /* essentialProperties= */ ImmutableList.of(), + /* supplementalProperties= */ ImmutableList.of(), /* cacheKey= */ null, /* contentLength= */ 1); RangedUri rangedUri = new RangedUri("path/to/resource", /* start= */ 0, /* length= */ 1); @@ -99,6 +101,8 @@ public final class DashUtilTest { baseUrls, new SingleSegmentBase(), /* inbandEventStreams= */ null, + /* essentialProperties= */ ImmutableList.of(), + /* supplementalProperties= */ ImmutableList.of(), "cacheKey", /* contentLength= */ 1); RangedUri rangedUri = new RangedUri("path/to/resource", /* start= */ 0, /* length= */ 1); 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 ce2961dd2e..6926a05859 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 @@ -24,6 +24,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.emsg.EventMessage; +import com.google.android.exoplayer2.source.dash.manifest.Representation.MultiSegmentRepresentation; +import com.google.android.exoplayer2.source.dash.manifest.Representation.SingleSegmentRepresentation; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.MimeTypes; @@ -53,6 +55,8 @@ public class DashManifestParserTest { private static final String SAMPLE_MPD_ASSET_IDENTIFIER = "media/mpd/sample_mpd_asset_identifier"; private static final String SAMPLE_MPD_TEXT = "media/mpd/sample_mpd_text"; private static final String SAMPLE_MPD_TRICK_PLAY = "media/mpd/sample_mpd_trick_play"; + private static final String SAMPLE_MPD_ESSENTIAL_SUPPLEMENTAL_PROPERTIES = + "media/mpd/sample_mpd_essential_supplemental_properties"; private static final String SAMPLE_MPD_AVAILABILITY_TIME_OFFSET_BASE_URL = "media/mpd/sample_mpd_availabilityTimeOffset_baseUrl"; private static final String SAMPLE_MPD_MULTIPLE_BASE_URLS = @@ -504,6 +508,74 @@ public class DashManifestParserTest { assertThat(assetIdentifier.id).isEqualTo("uniqueId"); } + @Test + public void parseEssentialAndSupplementalProperties() throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), + SAMPLE_MPD_ESSENTIAL_SUPPLEMENTAL_PROPERTIES)); + + // Verify test setup. + assertThat(manifest.getPeriodCount()).isEqualTo(1); + assertThat(manifest.getPeriod(0).adaptationSets).hasSize(1); + AdaptationSet adaptationSet = manifest.getPeriod(0).adaptationSets.get(0); + assertThat(adaptationSet.representations).hasSize(2); + Representation representation0 = adaptationSet.representations.get(0); + Representation representation1 = adaptationSet.representations.get(1); + assertThat(representation0).isInstanceOf(SingleSegmentRepresentation.class); + assertThat(representation1).isInstanceOf(MultiSegmentRepresentation.class); + + // Verify parsed properties. + assertThat(adaptationSet.essentialProperties).hasSize(1); + assertThat(adaptationSet.essentialProperties.get(0).schemeIdUri) + .isEqualTo("urn:mpeg:dash:essential-scheme:2050"); + assertThat(adaptationSet.essentialProperties.get(0).value).isEqualTo("adaptationEssential"); + assertThat(adaptationSet.supplementalProperties).hasSize(1); + assertThat(adaptationSet.supplementalProperties.get(0).schemeIdUri) + .isEqualTo("urn:mpeg:dash:supplemental-scheme:2050"); + assertThat(adaptationSet.supplementalProperties.get(0).value) + .isEqualTo("adaptationSupplemental"); + + assertThat(representation0.essentialProperties).hasSize(2); + assertThat(representation0.essentialProperties.get(0).schemeIdUri) + .isEqualTo("urn:mpeg:dash:essential-scheme:2050"); + assertThat(representation0.essentialProperties.get(0).value).isEqualTo("adaptationEssential"); + assertThat(representation0.essentialProperties.get(1).schemeIdUri) + .isEqualTo("urn:mpeg:dash:essential-scheme:2050"); + assertThat(representation0.essentialProperties.get(1).value) + .isEqualTo("representationEssential"); + assertThat(representation0.supplementalProperties).hasSize(2); + assertThat(representation0.supplementalProperties.get(0).schemeIdUri) + .isEqualTo("urn:mpeg:dash:supplemental-scheme:2050"); + assertThat(representation0.supplementalProperties.get(0).value) + .isEqualTo("adaptationSupplemental"); + assertThat(representation0.supplementalProperties.get(1).schemeIdUri) + .isEqualTo("urn:mpeg:dash:supplemental-scheme:2050"); + assertThat(representation0.supplementalProperties.get(1).value) + .isEqualTo("representationSupplemental"); + + assertThat(representation1.essentialProperties).hasSize(2); + assertThat(representation0.essentialProperties.get(0).schemeIdUri) + .isEqualTo("urn:mpeg:dash:essential-scheme:2050"); + assertThat(representation0.essentialProperties.get(0).value).isEqualTo("adaptationEssential"); + assertThat(representation1.essentialProperties.get(1).schemeIdUri) + .isEqualTo("urn:mpeg:dash:essential-scheme:2050"); + assertThat(representation1.essentialProperties.get(1).value) + .isEqualTo("representationEssential"); + assertThat(representation1.supplementalProperties).hasSize(2); + assertThat(representation0.supplementalProperties.get(0).schemeIdUri) + .isEqualTo("urn:mpeg:dash:supplemental-scheme:2050"); + assertThat(representation0.supplementalProperties.get(0).value) + .isEqualTo("adaptationSupplemental"); + assertThat(representation1.supplementalProperties.get(1).schemeIdUri) + .isEqualTo("urn:mpeg:dash:supplemental-scheme:2050"); + assertThat(representation1.supplementalProperties.get(1).value) + .isEqualTo("representationSupplemental"); + } + @Test public void availabilityTimeOffset_staticManifest_setToTimeUnset() throws IOException { DashManifestParser parser = new DashManifestParser(); diff --git a/library/transformer/build.gradle b/library/transformer/build.gradle index 32fa7695d5..28742d1f6f 100644 --- a/library/transformer/build.gradle +++ b/library/transformer/build.gradle @@ -12,15 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" - android { + + defaultConfig { + // The following argument makes the Android Test Orchestrator run its + // "pm clear" command after each test invocation. This command ensures + // that the app's state is completely cleared between tests. + testInstrumentationRunnerArguments clearPackageData: 'true' + multiDexEnabled true + } + buildTypes { debug { testCoverageEnabled = true } } - sourceSets.test.assets.srcDir '../../testdata/src/test/assets/' + sourceSets { + androidTest.assets.srcDir '../test_data/src/test/assets/' //copybara:media3-only + androidTest.assets.srcDir '../../testdata/src/test/assets/' + test.assets.srcDir '../../testdata/src/test/assets/' + } } dependencies { @@ -33,6 +45,10 @@ dependencies { testImplementation project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testdata') testImplementation 'org.robolectric:robolectric:' + robolectricVersion + testImplementation 'com.google.truth:truth:' + truthVersion + androidTestImplementation 'junit:junit:' + junitVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion + androidTestImplementation project(modulePrefix + 'testutils') } ext { diff --git a/library/transformer/src/androidTest/AndroidManifest.xml b/library/transformer/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..c1afec1685 --- /dev/null +++ b/library/transformer/src/androidTest/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/AndroidTestUtil.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/AndroidTestUtil.java new file mode 100644 index 0000000000..1afb81f667 --- /dev/null +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/AndroidTestUtil.java @@ -0,0 +1,91 @@ +/* + * 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 android.content.Context; +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.test.platform.app.InstrumentationRegistry; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.util.Assertions; +import java.io.File; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** Utility methods for instrumentation tests. */ +/* package */ final class AndroidTestUtil { + public static final String MP4_ASSET_URI = "asset:///media/mp4/sample.mp4"; + public static final String SEF_ASSET_URI = "asset:///media/mp4/sample_sef_slow_motion.mp4"; + + /** Transforms the {@code uriString} with the {@link Transformer}. */ + public static void runTransformer(Context context, Transformer transformer, String uriString) + throws Exception { + AtomicReference<@NullableType Exception> exceptionReference = new AtomicReference<>(); + CountDownLatch countDownLatch = new CountDownLatch(1); + + Transformer testTransformer = + transformer + .buildUpon() + .setListener( + new Transformer.Listener() { + @Override + public void onTransformationCompleted(MediaItem inputMediaItem) { + countDownLatch.countDown(); + } + + @Override + public void onTransformationError(MediaItem inputMediaItem, Exception exception) { + exceptionReference.set(exception); + countDownLatch.countDown(); + } + }) + .build(); + + Uri uri = Uri.parse(uriString); + File externalCacheFile = createExternalCacheFile(uri, context); + try { + InstrumentationRegistry.getInstrumentation() + .runOnMainSync( + () -> { + try { + testTransformer.startTransformation( + MediaItem.fromUri(uri), externalCacheFile.getAbsolutePath()); + } catch (IOException e) { + exceptionReference.set(e); + } + }); + countDownLatch.await(); + @Nullable Exception exception = exceptionReference.get(); + if (exception != null) { + throw exception; + } + } finally { + externalCacheFile.delete(); + } + } + + private static File createExternalCacheFile(Uri uri, Context context) throws IOException { + File file = new File(context.getExternalCacheDir(), "transformer-" + uri.hashCode()); + Assertions.checkState( + !file.exists() || file.delete(), "Could not delete the previous transformer output file"); + Assertions.checkState(file.createNewFile(), "Could not create the transformer output file"); + return file; + } + + private AndroidTestUtil() {} +} diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/RemoveAudioTransformationTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/RemoveAudioTransformationTest.java new file mode 100644 index 0000000000..b8c7bd923b --- /dev/null +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/RemoveAudioTransformationTest.java @@ -0,0 +1,37 @@ +/* + * 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.transformer.AndroidTestUtil.MP4_ASSET_URI; +import static com.google.android.exoplayer2.transformer.AndroidTestUtil.runTransformer; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** {@link Transformer} instrumentation test for removing audio. */ +@RunWith(AndroidJUnit4.class) +public class RemoveAudioTransformationTest { + @Test + public void removeAudioTransform() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = + new Transformer.Builder().setContext(context).setRemoveAudio(true).build(); + runTransformer(context, transformer, MP4_ASSET_URI); + } +} diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/RemoveVideoTransformationTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/RemoveVideoTransformationTest.java new file mode 100644 index 0000000000..7a27887c93 --- /dev/null +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/RemoveVideoTransformationTest.java @@ -0,0 +1,37 @@ +/* + * 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.transformer.AndroidTestUtil.MP4_ASSET_URI; +import static com.google.android.exoplayer2.transformer.AndroidTestUtil.runTransformer; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** {@link Transformer} instrumentation test for removing video. */ +@RunWith(AndroidJUnit4.class) +public class RemoveVideoTransformationTest { + @Test + public void removeVideoTransform() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = + new Transformer.Builder().setContext(context).setRemoveVideo(true).build(); + runTransformer(context, transformer, MP4_ASSET_URI); + } +} diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/SefTransformationTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/SefTransformationTest.java new file mode 100644 index 0000000000..5b311443b4 --- /dev/null +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/SefTransformationTest.java @@ -0,0 +1,37 @@ +/* + * 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.transformer.AndroidTestUtil.SEF_ASSET_URI; +import static com.google.android.exoplayer2.transformer.AndroidTestUtil.runTransformer; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** {@link Transformer} instrumentation test for SEF. */ +@RunWith(AndroidJUnit4.class) +public class SefTransformationTest { + @Test + public void sefTransform() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = + new Transformer.Builder().setContext(context).setFlattenForSlowMotion(true).build(); + runTransformer(context, transformer, SEF_ASSET_URI); + } +} diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformationTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformationTest.java new file mode 100644 index 0000000000..20fe021ff2 --- /dev/null +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformationTest.java @@ -0,0 +1,36 @@ +/* + * 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.transformer.AndroidTestUtil.MP4_ASSET_URI; +import static com.google.android.exoplayer2.transformer.AndroidTestUtil.runTransformer; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** {@link Transformer} instrumentation test. */ +@RunWith(AndroidJUnit4.class) +public class TransformationTest { + @Test + public void transform() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = new Transformer.Builder().setContext(context).build(); + runTransformer(context, transformer, MP4_ASSET_URI); + } +} diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/OpenGlFrameEditor.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/OpenGlFrameEditor.java new file mode 100644 index 0000000000..deab55d692 --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/OpenGlFrameEditor.java @@ -0,0 +1,180 @@ +/* + * 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.content.Context; +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLExt; +import android.opengl.EGLSurface; +import android.opengl.GLES20; +import android.view.Surface; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.GlUtil; +import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** Applies OpenGL transformations to video frames. */ +@RequiresApi(18) +/* package */ final class OpenGlFrameEditor { + + static { + GlUtil.glAssertionsEnabled = true; + } + + public static OpenGlFrameEditor create( + Context context, Format inputFormat, Surface outputSurface) { + EGLDisplay eglDisplay = GlUtil.createEglDisplay(); + EGLContext eglContext; + try { + eglContext = GlUtil.createEglContext(eglDisplay); + } catch (GlUtil.UnsupportedEglVersionException e) { + throw new IllegalStateException("EGL version is unsupported", e); + } + EGLSurface eglSurface = GlUtil.getEglSurface(eglDisplay, outputSurface); + GlUtil.focusSurface(eglDisplay, eglContext, eglSurface, inputFormat.width, inputFormat.height); + int textureId = GlUtil.createExternalTexture(); + GlUtil.Program copyProgram; + try { + copyProgram = new GlUtil.Program(context, VERTEX_SHADER_FILE_PATH, FRAGMENT_SHADER_FILE_PATH); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + copyProgram.use(); + GlUtil.Attribute[] copyAttributes = copyProgram.getAttributes(); + checkState( + copyAttributes.length == EXPECTED_NUMBER_OF_ATTRIBUTES, + "Expected program to have " + EXPECTED_NUMBER_OF_ATTRIBUTES + " vertex attributes."); + for (GlUtil.Attribute copyAttribute : copyAttributes) { + if (copyAttribute.name.equals("a_position")) { + copyAttribute.setBuffer( + new float[] { + -1.0f, -1.0f, 0.0f, 1.0f, + 1.0f, -1.0f, 0.0f, 1.0f, + -1.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 0.0f, 1.0f, + }, + /* size= */ 4); + } else if (copyAttribute.name.equals("a_texcoord")) { + copyAttribute.setBuffer( + new float[] { + 0.0f, 0.0f, 0.0f, 1.0f, + 1.0f, 0.0f, 0.0f, 1.0f, + 0.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 0.0f, 1.0f, + }, + /* size= */ 4); + } else { + throw new IllegalStateException("Unexpected attribute name."); + } + copyAttribute.bind(); + } + GlUtil.Uniform[] copyUniforms = copyProgram.getUniforms(); + checkState( + copyUniforms.length == EXPECTED_NUMBER_OF_UNIFORMS, + "Expected program to have " + EXPECTED_NUMBER_OF_UNIFORMS + " uniforms."); + GlUtil.@MonotonicNonNull Uniform textureTransformUniform = null; + for (GlUtil.Uniform copyUniform : copyUniforms) { + if (copyUniform.name.equals("tex_sampler")) { + copyUniform.setSamplerTexId(textureId, 0); + copyUniform.bind(); + } else if (copyUniform.name.equals("tex_transform")) { + textureTransformUniform = copyUniform; + } else { + throw new IllegalStateException("Unexpected uniform name."); + } + } + + return new OpenGlFrameEditor( + eglDisplay, eglContext, eglSurface, textureId, checkNotNull(textureTransformUniform)); + } + + // Predefined shader values. + private static final String VERTEX_SHADER_FILE_PATH = "shaders/blit_vertex_shader.glsl"; + private static final String FRAGMENT_SHADER_FILE_PATH = + "shaders/copy_external_fragment_shader.glsl"; + private static final int EXPECTED_NUMBER_OF_ATTRIBUTES = 2; + private static final int EXPECTED_NUMBER_OF_UNIFORMS = 2; + + private final float[] textureTransformMatrix; + private final EGLDisplay eglDisplay; + private final EGLContext eglContext; + private final EGLSurface eglSurface; + private final int textureId; + private final SurfaceTexture inputSurfaceTexture; + private final Surface inputSurface; + private final GlUtil.Uniform textureTransformUniform; + + private volatile boolean hasInputData; + + private OpenGlFrameEditor( + EGLDisplay eglDisplay, + EGLContext eglContext, + EGLSurface eglSurface, + int textureId, + GlUtil.Uniform textureTransformUniform) { + this.eglDisplay = eglDisplay; + this.eglContext = eglContext; + this.eglSurface = eglSurface; + this.textureId = textureId; + this.textureTransformUniform = textureTransformUniform; + textureTransformMatrix = new float[16]; + inputSurfaceTexture = new SurfaceTexture(textureId); + inputSurfaceTexture.setOnFrameAvailableListener(surfaceTexture -> hasInputData = true); + inputSurface = new Surface(inputSurfaceTexture); + } + + /** Releases all resources. */ + public void release() { + GlUtil.destroyEglContext(eglDisplay, eglContext); + GlUtil.deleteTexture(textureId); + inputSurfaceTexture.release(); + inputSurface.release(); + } + + /** Informs the editor that there is new input data available for it to process asynchronously. */ + public void processData() { + inputSurfaceTexture.updateTexImage(); + inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); + textureTransformUniform.setFloats(textureTransformMatrix); + textureTransformUniform.bind(); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + long surfaceTextureTimestampNs = inputSurfaceTexture.getTimestamp(); + EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, surfaceTextureTimestampNs); + EGL14.eglSwapBuffers(eglDisplay, eglSurface); + hasInputData = false; + } + + /** + * Returns the input {@link Surface} after configuring the editor if it has not previously been + * configured. + */ + public Surface getInputSurface() { + return inputSurface; + } + + public boolean hasInputData() { + return hasInputData; + } +} 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 004331404d..f6a0d4a433 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 @@ -17,19 +17,9 @@ 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 static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; import android.content.Context; -import android.graphics.SurfaceTexture; import android.media.MediaCodec; -import android.opengl.EGL14; -import android.opengl.EGLContext; -import android.opengl.EGLDisplay; -import android.opengl.EGLExt; -import android.opengl.EGLSurface; -import android.opengl.GLES20; -import android.view.Surface; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; @@ -37,13 +27,8 @@ 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.android.exoplayer2.util.GlUtil; import com.google.common.collect.ImmutableMap; import java.io.IOException; -import org.checkerframework.checker.nullness.qual.EnsuresNonNull; -import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Pipeline to decode video samples, apply transformations on the raw samples, and re-encode them. @@ -51,52 +36,24 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @RequiresApi(18) /* package */ final class VideoSamplePipeline implements SamplePipeline { - static { - GlUtil.glAssertionsEnabled = true; - } - private static final String TAG = "VideoSamplePipeline"; - // Predefined shader values. - private static final String VERTEX_SHADER_FILE_PATH = "shaders/blit_vertex_shader.glsl"; - private static final String FRAGMENT_SHADER_FILE_PATH = - "shaders/copy_external_fragment_shader.glsl"; - private static final int EXPECTED_NUMBER_OF_ATTRIBUTES = 2; - private static final int EXPECTED_NUMBER_OF_UNIFORMS = 2; - - private final Context context; - private final int rendererIndex; - private final MediaCodecAdapterWrapper encoder; private final DecoderInputBuffer encoderOutputBuffer; private final DecoderInputBuffer decoderInputBuffer; - private final float[] decoderTextureTransformMatrix; - private final Format decoderInputFormat; + private final MediaCodecAdapterWrapper decoder; - private @MonotonicNonNull EGLDisplay eglDisplay; - private @MonotonicNonNull EGLContext eglContext; - private @MonotonicNonNull EGLSurface eglSurface; + private final OpenGlFrameEditor openGlFrameEditor; - private int decoderTextureId; - private @MonotonicNonNull SurfaceTexture decoderSurfaceTexture; - private @MonotonicNonNull Surface decoderSurface; - private @MonotonicNonNull MediaCodecAdapterWrapper decoder; - private volatile boolean isDecoderSurfacePopulated; private boolean waitingForPopulatedDecoderSurface; - private GlUtil.@MonotonicNonNull Uniform decoderTextureTransformUniform; public VideoSamplePipeline( Context context, Format decoderInputFormat, Transformation transformation, int rendererIndex) throws ExoPlaybackException { - this.decoderInputFormat = decoderInputFormat; - this.rendererIndex = rendererIndex; - this.context = context; decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); - decoderTextureTransformMatrix = new float[16]; - decoderTextureId = GlUtil.TEXTURE_ID_UNSET; encoderOutputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); @@ -114,34 +71,57 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ImmutableMap.of()); } catch (IOException e) { // TODO (internal b/184262323): Assign an adequate error code. - throw ExoPlaybackException.createForRenderer( - e, - TAG, - rendererIndex, - decoderInputFormat, - /* rendererFormatSupport= */ C.FORMAT_HANDLED, - /* isRecoverable= */ false, - PlaybackException.ERROR_CODE_UNSPECIFIED); + throw createRendererException( + e, rendererIndex, decoderInputFormat, PlaybackException.ERROR_CODE_UNSPECIFIED); + } + openGlFrameEditor = + OpenGlFrameEditor.create( + context, + decoderInputFormat, + /* outputSurface= */ checkNotNull(encoder.getInputSurface())); + try { + decoder = + MediaCodecAdapterWrapper.createForVideoDecoding( + decoderInputFormat, openGlFrameEditor.getInputSurface()); + } catch (IOException e) { + throw createRendererException( + e, rendererIndex, decoderInputFormat, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED); } } @Override - public boolean processData() throws ExoPlaybackException { - ensureOpenGlConfigured(); - return !ensureDecoderConfigured() || feedEncoderFromDecoder(); + public boolean processData() { + if (decoder.isEnded()) { + return false; + } + + if (!openGlFrameEditor.hasInputData()) { + if (!waitingForPopulatedDecoderSurface) { + if (decoder.getOutputBufferInfo() != null) { + decoder.releaseOutputBuffer(/* render= */ true); + waitingForPopulatedDecoderSurface = true; + } + if (decoder.isEnded()) { + encoder.signalEndOfInputStream(); + } + } + return false; + } + + waitingForPopulatedDecoderSurface = false; + openGlFrameEditor.processData(); + return true; } @Override @Nullable public DecoderInputBuffer dequeueInputBuffer() { - return decoder != null && decoder.maybeDequeueInputBuffer(decoderInputBuffer) - ? decoderInputBuffer - : null; + return decoder.maybeDequeueInputBuffer(decoderInputBuffer) ? decoderInputBuffer : null; } @Override public void queueInputBuffer() { - checkStateNotNull(decoder).queueInputBuffer(decoderInputBuffer); + decoder.queueInputBuffer(decoderInputBuffer); } @Override @@ -175,154 +155,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override public void release() { - GlUtil.destroyEglContext(eglDisplay, eglContext); - if (decoderTextureId != GlUtil.TEXTURE_ID_UNSET) { - GlUtil.deleteTexture(decoderTextureId); - } - if (decoderSurfaceTexture != null) { - decoderSurfaceTexture.release(); - } - if (decoderSurface != null) { - decoderSurface.release(); - } - if (decoder != null) { - decoder.release(); - } + openGlFrameEditor.release(); + decoder.release(); encoder.release(); } - @EnsuresNonNull({"eglDisplay", "eglContext", "eglSurface", "decoderTextureTransformUniform"}) - private void ensureOpenGlConfigured() { - if (eglDisplay != null - && eglContext != null - && eglSurface != null - && decoderTextureTransformUniform != null) { - return; - } - - eglDisplay = GlUtil.createEglDisplay(); - try { - eglContext = GlUtil.createEglContext(eglDisplay); - } catch (GlUtil.UnsupportedEglVersionException e) { - throw new IllegalStateException("EGL version is unsupported", e); - } - eglSurface = GlUtil.getEglSurface(eglDisplay, checkNotNull(encoder.getInputSurface())); - GlUtil.focusSurface( - eglDisplay, eglContext, eglSurface, decoderInputFormat.width, decoderInputFormat.height); - decoderTextureId = GlUtil.createExternalTexture(); - GlUtil.Program copyProgram; - try { - copyProgram = new GlUtil.Program(context, VERTEX_SHADER_FILE_PATH, FRAGMENT_SHADER_FILE_PATH); - } catch (IOException e) { - throw new IllegalStateException(e); - } - - copyProgram.use(); - GlUtil.Attribute[] copyAttributes = copyProgram.getAttributes(); - checkState( - copyAttributes.length == EXPECTED_NUMBER_OF_ATTRIBUTES, - "Expected program to have " + EXPECTED_NUMBER_OF_ATTRIBUTES + " vertex attributes."); - for (GlUtil.Attribute copyAttribute : copyAttributes) { - if (copyAttribute.name.equals("a_position")) { - copyAttribute.setBuffer( - new float[] { - -1.0f, -1.0f, 0.0f, 1.0f, - 1.0f, -1.0f, 0.0f, 1.0f, - -1.0f, 1.0f, 0.0f, 1.0f, - 1.0f, 1.0f, 0.0f, 1.0f, - }, - /* size= */ 4); - } else if (copyAttribute.name.equals("a_texcoord")) { - copyAttribute.setBuffer( - new float[] { - 0.0f, 0.0f, 0.0f, 1.0f, - 1.0f, 0.0f, 0.0f, 1.0f, - 0.0f, 1.0f, 0.0f, 1.0f, - 1.0f, 1.0f, 0.0f, 1.0f, - }, - /* size= */ 4); - } else { - throw new IllegalStateException("Unexpected attribute name."); - } - copyAttribute.bind(); - } - GlUtil.Uniform[] copyUniforms = copyProgram.getUniforms(); - checkState( - copyUniforms.length == EXPECTED_NUMBER_OF_UNIFORMS, - "Expected program to have " + EXPECTED_NUMBER_OF_UNIFORMS + " uniforms."); - for (GlUtil.Uniform copyUniform : copyUniforms) { - if (copyUniform.name.equals("tex_sampler")) { - copyUniform.setSamplerTexId(decoderTextureId, 0); - copyUniform.bind(); - } else if (copyUniform.name.equals("tex_transform")) { - decoderTextureTransformUniform = copyUniform; - } else { - throw new IllegalStateException("Unexpected uniform name."); - } - } - checkNotNull(decoderTextureTransformUniform); - } - - @EnsuresNonNullIf( - expression = {"decoder", "decoderSurfaceTexture"}, - result = true) - private boolean ensureDecoderConfigured() throws ExoPlaybackException { - if (decoder != null && decoderSurfaceTexture != null) { - return true; - } - - checkState(decoderTextureId != GlUtil.TEXTURE_ID_UNSET); - decoderSurfaceTexture = new SurfaceTexture(decoderTextureId); - decoderSurfaceTexture.setOnFrameAvailableListener( - surfaceTexture -> isDecoderSurfacePopulated = true); - decoderSurface = new Surface(decoderSurfaceTexture); - try { - decoder = MediaCodecAdapterWrapper.createForVideoDecoding(decoderInputFormat, decoderSurface); - } catch (IOException e) { - throw createRendererException(e, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED); - } - return true; - } - - @RequiresNonNull({ - "decoder", - "decoderSurfaceTexture", - "decoderTextureTransformUniform", - "eglDisplay", - "eglSurface" - }) - private boolean feedEncoderFromDecoder() { - if (decoder.isEnded()) { - return false; - } - - if (!isDecoderSurfacePopulated) { - if (!waitingForPopulatedDecoderSurface) { - if (decoder.getOutputBufferInfo() != null) { - decoder.releaseOutputBuffer(/* render= */ true); - waitingForPopulatedDecoderSurface = true; - } - if (decoder.isEnded()) { - encoder.signalEndOfInputStream(); - } - } - return false; - } - - waitingForPopulatedDecoderSurface = false; - decoderSurfaceTexture.updateTexImage(); - decoderSurfaceTexture.getTransformMatrix(decoderTextureTransformMatrix); - decoderTextureTransformUniform.setFloats(decoderTextureTransformMatrix); - decoderTextureTransformUniform.bind(); - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); - long decoderSurfaceTextureTimestampNs = decoderSurfaceTexture.getTimestamp(); - EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, decoderSurfaceTextureTimestampNs); - EGL14.eglSwapBuffers(eglDisplay, eglSurface); - isDecoderSurfacePopulated = false; - return true; - } - - private ExoPlaybackException createRendererException(Throwable cause, int errorCode) { + private static ExoPlaybackException createRendererException( + Throwable cause, int rendererIndex, Format decoderInputFormat, int errorCode) { return ExoPlaybackException.createForRenderer( cause, TAG, diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index a7838377bf..e880b0f254 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -1222,7 +1222,6 @@ public class PlayerControlView extends FrameLayout { } } - @SuppressWarnings("deprecation") private void dispatchPlay(Player player) { @State int state = player.getPlaybackState(); if (state == Player.STATE_IDLE) { 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 cd33ecc9d9..4c1f0aa74c 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 @@ -2165,7 +2165,9 @@ public class StyledPlayerControlView extends FrameLayout { TrackSelectionParameters trackSelectionParameters = player.getTrackSelectionParameters(); TrackSelectionOverrides overrides = - new TrackSelectionOverrides.Builder() + trackSelectionParameters + .trackSelectionOverrides + .buildUpon() .setOverrideForType( new TrackSelectionOverride( track.trackGroup, ImmutableList.of(track.trackIndex))) diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_essential_supplemental_properties b/testdata/src/test/assets/media/mpd/sample_mpd_essential_supplemental_properties new file mode 100644 index 0000000000..1ef8a7e7f1 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_essential_supplemental_properties @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + +