From d59cfb736a86f3dd7cf93b50a06fdce022bcb6b6 Mon Sep 17 00:00:00 2001 From: claincly Date: Fri, 7 Jan 2022 17:23:00 +0000 Subject: [PATCH 01/28] Replace static method with a static field. PiperOrigin-RevId: 420307694 --- .../transformer/DefaultCodecFactory.java | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) 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 index c6ace32a6b..22603002c4 100644 --- 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 @@ -36,6 +36,18 @@ import java.io.IOException; /* package */ final class DefaultCodecFactory implements Codec.DecoderFactory, Codec.EncoderFactory { + private static final MediaCodecInfo PLACEHOLDER_MEDIA_CODEC_INFO = + 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); + @Override public Codec createForAudioDecoding(Format format) throws TransformationException { MediaFormat mediaFormat = @@ -51,7 +63,7 @@ import java.io.IOException; new MediaCodecFactory() .createAdapter( MediaCodecAdapter.Configuration.createForAudioDecoding( - createPlaceholderMediaCodecInfo(), mediaFormat, format, /* crypto= */ null)); + PLACEHOLDER_MEDIA_CODEC_INFO, mediaFormat, format, /* crypto= */ null)); } catch (Exception e) { throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ true); } @@ -81,7 +93,7 @@ import java.io.IOException; new MediaCodecFactory() .createAdapter( MediaCodecAdapter.Configuration.createForVideoDecoding( - createPlaceholderMediaCodecInfo(), + PLACEHOLDER_MEDIA_CODEC_INFO, mediaFormat, format, surface, @@ -105,7 +117,7 @@ import java.io.IOException; new MediaCodecFactory() .createAdapter( MediaCodecAdapter.Configuration.createForAudioEncoding( - createPlaceholderMediaCodecInfo(), mediaFormat, format)); + PLACEHOLDER_MEDIA_CODEC_INFO, mediaFormat, format)); } catch (Exception e) { throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ false); } @@ -135,7 +147,7 @@ import java.io.IOException; new MediaCodecFactory() .createAdapter( MediaCodecAdapter.Configuration.createForVideoEncoding( - createPlaceholderMediaCodecInfo(), mediaFormat, format)); + PLACEHOLDER_MEDIA_CODEC_INFO, mediaFormat, format)); } catch (Exception e) { throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ false); } @@ -155,19 +167,6 @@ import java.io.IOException; } } - 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"); From 93af4ad4a72ae0dfac9431032e2ff403de556095 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 10 Jan 2022 10:22:24 +0000 Subject: [PATCH 02/28] Add tests for DefaultTrackSelector handling of forced & default tracks Issue: google/ExoPlayer#9797 PiperOrigin-RevId: 420707176 --- .../DefaultTrackSelectorTest.java | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index db5f4712e9..40a8e3d93c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -1167,16 +1167,30 @@ public final class DefaultTrackSelectorTest { } /** - * Tests that the default track selector will select a forced text track matching the selected - * audio language when no text language preferences match. + * Tests that the default track selector will select: + * + * */ @Test - public void selectingForcedTextTrackMatchesAudioLanguage() throws ExoPlaybackException { - Format.Builder formatBuilder = + public void forcedAndDefaultTextTracksInteractWithSelectedAudioLanguageAsExpected() + throws ExoPlaybackException { + Format.Builder forcedTextBuilder = TEXT_FORMAT.buildUpon().setSelectionFlags(C.SELECTION_FLAG_FORCED); - Format forcedEnglish = formatBuilder.setLanguage("eng").build(); - Format forcedGerman = formatBuilder.setLanguage("deu").build(); - Format forcedNoLanguage = formatBuilder.setLanguage(C.LANGUAGE_UNDETERMINED).build(); + Format.Builder defaultTextBuilder = + TEXT_FORMAT.buildUpon().setSelectionFlags(C.SELECTION_FLAG_DEFAULT); + Format forcedEnglish = forcedTextBuilder.setLanguage("eng").build(); + Format defaultEnglish = defaultTextBuilder.setLanguage("eng").build(); + Format forcedGerman = forcedTextBuilder.setLanguage("deu").build(); + Format defaultGerman = defaultTextBuilder.setLanguage("deu").build(); + Format forcedNoLanguage = forcedTextBuilder.setLanguage(C.LANGUAGE_UNDETERMINED).build(); Format noLanguageAudio = AUDIO_FORMAT.buildUpon().setLanguage(null).build(); Format germanAudio = AUDIO_FORMAT.buildUpon().setLanguage("deu").build(); @@ -1209,6 +1223,19 @@ public final class DefaultTrackSelectorTest { trackGroups = wrapFormats(germanAudio, forcedEnglish, forcedGerman); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections[1], trackGroups, forcedGerman); + + // The audio declares german. The default german track should be selected (in favour of the + // default english track and forced german track). + trackGroups = + wrapFormats(germanAudio, forcedGerman, defaultGerman, forcedEnglish, defaultEnglish); + result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); + assertFixedSelection(result.selections[1], trackGroups, defaultGerman); + + // The audio declares german. The default english track should be selected because there's no + // default german track. + trackGroups = wrapFormats(germanAudio, forcedGerman, forcedEnglish, defaultEnglish); + result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); + assertFixedSelection(result.selections[1], trackGroups, defaultEnglish); } /** From 0af7f5c28748a5936443cf815070ae7efdf825fe Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 10 Jan 2022 10:24:31 +0000 Subject: [PATCH 03/28] Fix spherical scene rendering The draw method was disabling vertex attrib arrays but not re-enabling them. Remove the call to disable the vertex attrib arrays so that then remain enabled after the program is created. Manually verified by setting the surface type to spherical in the demo app and playing a spherical sample video. Issue: google/ExoPlayer#9782 PiperOrigin-RevId: 420707503 --- .../android/exoplayer2/video/spherical/ProjectionRenderer.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionRenderer.java index 3ac9bdeb70..4326f7917f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionRenderer.java @@ -178,9 +178,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // Render. GLES20.glDrawArrays(meshData.drawMode, /* first= */ 0, meshData.vertexCount); checkGlError(); - - GLES20.glDisableVertexAttribArray(positionHandle); - GLES20.glDisableVertexAttribArray(texCoordsHandle); } /** Cleans up GL resources. */ From 103b170a562b51935142b0d9049212de6da2b985 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 10 Jan 2022 10:26:16 +0000 Subject: [PATCH 04/28] Rename some references from PlayerView to LegacyPlayerView These were missed in https://github.com/google/ExoPlayer/commit/46ab94bd4152b0addc2253585435186fc9e75582 These references will be re-written to PlayerView when exporting to exoplayer2, so this commit results in some small reformatting changes. Also fix a reference to LegacyPlayerControlView that should be StyledPlayerControlView. PiperOrigin-RevId: 420707706 --- docs/ad-insertion.md | 16 ++++++++-------- docs/hls.md | 1 - extensions/av1/README.md | 16 ++++++++-------- extensions/vp9/README.md | 16 ++++++++-------- library/ui/src/main/res/values/attrs.xml | 2 +- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/docs/ad-insertion.md b/docs/ad-insertion.md index 142de570a4..369b4427fc 100644 --- a/docs/ad-insertion.md +++ b/docs/ad-insertion.md @@ -64,9 +64,9 @@ Internally, `DefaultMediaSourceFactory` will wrap the content media source in an `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 -`AdViewProvider`. The IMA extension provides an easy to use `AdsLoader`, as -described below. +ExoPlayer's `StyledPlayerView` and `PlayerView` UI components both +implement `AdViewProvider`. The IMA extension provides an easy to use +`AdsLoader`, as described below. ### Playlists with ads ### @@ -123,8 +123,8 @@ VAST/VMAP ad tags in the sample list. #### UI considerations #### -`StyledPlayerView` and `PlayerView` hide controls during playback of ads by -default, but apps can toggle this behavior by calling +`StyledPlayerView` and `PlayerView` hide controls during playback of ads +by default, but apps can toggle this behavior by calling `setControllerHideDuringAds`, which is defined on both views. The IMA SDK will show additional views on top of the player while an ad is playing (e.g., a 'more info' link and a skip button, if applicable). @@ -139,9 +139,9 @@ The IMA SDK may report whether ads are obscured by application provided views rendered on top of the player. Apps that need to overlay views that are essential for controlling playback must register them with the IMA SDK so that they can be omitted from viewability calculations. When using `StyledPlayerView` -or `PlayerView` as the `AdViewProvider`, they will automatically register their -control overlays. Apps that use a custom player UI must register overlay views -by returning them from `AdViewProvider.getAdOverlayInfos`. +or `PlayerView` as the `AdViewProvider`, they will automatically register +their control overlays. Apps that use a custom player UI must register overlay +views by returning them from `AdViewProvider.getAdOverlayInfos`. For more information about overlay views, see [Open Measurement in the IMA SDK][]. diff --git a/docs/hls.md b/docs/hls.md index 5633ad8976..fbea55ff4e 100644 --- a/docs/hls.md +++ b/docs/hls.md @@ -130,7 +130,6 @@ The following guidelines apply specifically for live streams: [HlsMediaSource]: {{ site.exo_sdk }}/source/hls/HlsMediaSource.html [HTTP Live Streaming]: https://tools.ietf.org/html/rfc8216 -[PlayerView]: {{ site.exo_sdk }}/ui/PlayerView.html [UI components]: {{ site.baseurl }}/ui-components.html [Customization page]: {{ site.baseurl }}/customization.html [Medium post about HLS playback in ExoPlayer]: https://medium.com/google-exoplayer/hls-playback-in-exoplayer-a33959a47be7 diff --git a/extensions/av1/README.md b/extensions/av1/README.md index f2a31dcbf1..b5d16dca8f 100644 --- a/extensions/av1/README.md +++ b/extensions/av1/README.md @@ -112,20 +112,20 @@ gets from the libgav1 decoder: * GL rendering using GL shader for color space conversion - * If you are using `ExoPlayer` with `PlayerView` or `StyledPlayerView`, - enable this option by setting `surface_type` of view to be - `video_decoder_gl_surface_view`. + * If you are using `ExoPlayer` with `PlayerView` or + `StyledPlayerView`, enable this option by setting `surface_type` of view + to be `video_decoder_gl_surface_view`. * Otherwise, enable this option by sending `Libgav1VideoRenderer` a - message of type `Renderer.MSG_SET_VIDEO_OUTPUT` - with an instance of `VideoDecoderOutputBufferRenderer` as its object. + message of type `Renderer.MSG_SET_VIDEO_OUTPUT` with an instance of + `VideoDecoderOutputBufferRenderer` as its object. `VideoDecoderGLSurfaceView` is the concrete `VideoDecoderOutputBufferRenderer` implementation used by - `(Styled)PlayerView`. + `PlayerView` and `StyledPlayerView`. * Native rendering using `ANativeWindow` - * If you are using `ExoPlayer` with `PlayerView` or `StyledPlayerView`, - this option is enabled by default. + * If you are using `ExoPlayer` with `PlayerView` or + `StyledPlayerView`, this option is enabled by default. * Otherwise, enable this option by sending `Libgav1VideoRenderer` a message of type `Renderer.MSG_SET_VIDEO_OUTPUT` with an instance of `SurfaceView` as its object. diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 384064d68d..2e471ca118 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -125,20 +125,20 @@ gets from the libvpx decoder: * GL rendering using GL shader for color space conversion - * If you are using `ExoPlayer` with `PlayerView` or `StyledPlayerView`, - enable this option by setting `surface_type` of view to be - `video_decoder_gl_surface_view`. + * If you are using `ExoPlayer` with `PlayerView` or + `StyledPlayerView`, enable this option by setting `surface_type` of view + to be `video_decoder_gl_surface_view`. * Otherwise, enable this option by sending `LibvpxVideoRenderer` a message - of type `Renderer.MSG_SET_VIDEO_OUTPUT` with an - instance of `VideoDecoderOutputBufferRenderer` as its object. + of type `Renderer.MSG_SET_VIDEO_OUTPUT` with an instance of + `VideoDecoderOutputBufferRenderer` as its object. `VideoDecoderGLSurfaceView` is the concrete `VideoDecoderOutputBufferRenderer` implementation used by - `(Styled)PlayerView`. + `PlayerView` and `StyledPlayerView`. * Native rendering using `ANativeWindow` - * If you are using `ExoPlayer` with `PlayerView` or `StyledPlayerView`, - this option is enabled by default. + * If you are using `ExoPlayer` with `PlayerView` or + `StyledPlayerView`, this option is enabled by default. * Otherwise, enable this option by sending `LibvpxVideoRenderer` a message of type `Renderer.MSG_SET_VIDEO_OUTPUT` with an instance of `SurfaceView` as its object. diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index a0547b53fb..da1eea4134 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -105,7 +105,7 @@ - + From 915f091ebdea73d4ff14f3765410cd435ece5af8 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 10 Jan 2022 10:50:24 +0000 Subject: [PATCH 05/28] Remove 'styled' from styledPlayerControlView field name This class is already called StyledPlayerControlViewLayoutManager, it seems unecessary to repeate the 'styled' word again in this context. PiperOrigin-RevId: 420711161 --- .../StyledPlayerControlViewLayoutManager.java | 89 +++++++++---------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlViewLayoutManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlViewLayoutManager.java index 8c03272b3b..57090630f7 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlViewLayoutManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlViewLayoutManager.java @@ -48,7 +48,7 @@ import java.util.List; // Int for defining the UX state where the views are being animated to be shown. private static final int UX_STATE_ANIMATING_SHOW = 4; - private final StyledPlayerControlView styledPlayerControlView; + private final StyledPlayerControlView playerControlView; @Nullable private final View controlsBackground; @Nullable private final ViewGroup centerControls; @@ -84,8 +84,8 @@ import java.util.List; private boolean animationEnabled; @SuppressWarnings({"nullness:method.invocation", "nullness:methodref.receiver.bound"}) - public StyledPlayerControlViewLayoutManager(StyledPlayerControlView styledPlayerControlView) { - this.styledPlayerControlView = styledPlayerControlView; + public StyledPlayerControlViewLayoutManager(StyledPlayerControlView playerControlView) { + this.playerControlView = playerControlView; showAllBarsRunnable = this::showAllBars; hideAllBarsRunnable = this::hideAllBars; hideProgressBarRunnable = this::hideProgressBar; @@ -97,26 +97,25 @@ import java.util.List; shownButtons = new ArrayList<>(); // Relating to Center View - controlsBackground = styledPlayerControlView.findViewById(R.id.exo_controls_background); - centerControls = styledPlayerControlView.findViewById(R.id.exo_center_controls); + controlsBackground = playerControlView.findViewById(R.id.exo_controls_background); + centerControls = playerControlView.findViewById(R.id.exo_center_controls); // Relating to Minimal Layout - minimalControls = styledPlayerControlView.findViewById(R.id.exo_minimal_controls); + minimalControls = playerControlView.findViewById(R.id.exo_minimal_controls); // Relating to Bottom Bar View - bottomBar = styledPlayerControlView.findViewById(R.id.exo_bottom_bar); + bottomBar = playerControlView.findViewById(R.id.exo_bottom_bar); // Relating to Bottom Bar Left View - timeView = styledPlayerControlView.findViewById(R.id.exo_time); - timeBar = styledPlayerControlView.findViewById(R.id.exo_progress); + timeView = playerControlView.findViewById(R.id.exo_time); + timeBar = playerControlView.findViewById(R.id.exo_progress); // Relating to Bottom Bar Right View - basicControls = styledPlayerControlView.findViewById(R.id.exo_basic_controls); - extraControls = styledPlayerControlView.findViewById(R.id.exo_extra_controls); - extraControlsScrollView = - styledPlayerControlView.findViewById(R.id.exo_extra_controls_scroll_view); - overflowShowButton = styledPlayerControlView.findViewById(R.id.exo_overflow_show); - View overflowHideButton = styledPlayerControlView.findViewById(R.id.exo_overflow_hide); + basicControls = playerControlView.findViewById(R.id.exo_basic_controls); + extraControls = playerControlView.findViewById(R.id.exo_extra_controls); + extraControlsScrollView = playerControlView.findViewById(R.id.exo_extra_controls_scroll_view); + overflowShowButton = playerControlView.findViewById(R.id.exo_overflow_show); + View overflowHideButton = playerControlView.findViewById(R.id.exo_overflow_hide); if (overflowShowButton != null && overflowHideButton != null) { overflowShowButton.setOnClickListener(this::onOverflowButtonClick); overflowHideButton.setOnClickListener(this::onOverflowButtonClick); @@ -194,7 +193,7 @@ import java.util.List; } }); - Resources resources = styledPlayerControlView.getResources(); + Resources resources = playerControlView.getResources(); float translationYForProgressBar = resources.getDimension(R.dimen.exo_styled_bottom_bar_height) - resources.getDimension(R.dimen.exo_styled_progress_bar_height); @@ -213,7 +212,7 @@ import java.util.List; public void onAnimationEnd(Animator animation) { setUxState(UX_STATE_ONLY_PROGRESS_VISIBLE); if (needToShowBars) { - styledPlayerControlView.post(showAllBarsRunnable); + playerControlView.post(showAllBarsRunnable); needToShowBars = false; } } @@ -236,7 +235,7 @@ import java.util.List; public void onAnimationEnd(Animator animation) { setUxState(UX_STATE_NONE_VISIBLE); if (needToShowBars) { - styledPlayerControlView.post(showAllBarsRunnable); + playerControlView.post(showAllBarsRunnable); needToShowBars = false; } } @@ -258,7 +257,7 @@ import java.util.List; public void onAnimationEnd(Animator animation) { setUxState(UX_STATE_NONE_VISIBLE); if (needToShowBars) { - styledPlayerControlView.post(showAllBarsRunnable); + playerControlView.post(showAllBarsRunnable); needToShowBars = false; } } @@ -352,10 +351,10 @@ import java.util.List; } public void show() { - if (!styledPlayerControlView.isVisible()) { - styledPlayerControlView.setVisibility(View.VISIBLE); - styledPlayerControlView.updateAll(); - styledPlayerControlView.requestPlayPauseFocus(); + if (!playerControlView.isVisible()) { + playerControlView.setVisibility(View.VISIBLE); + playerControlView.updateAll(); + playerControlView.requestPlayPauseFocus(); } showAllBars(); } @@ -395,7 +394,7 @@ import java.util.List; return; } removeHideCallbacks(); - int showTimeoutMs = styledPlayerControlView.getShowTimeoutMs(); + int showTimeoutMs = playerControlView.getShowTimeoutMs(); if (showTimeoutMs > 0) { if (!animationEnabled) { postDelayedRunnable(hideControllerRunnable, showTimeoutMs); @@ -408,22 +407,22 @@ import java.util.List; } public void removeHideCallbacks() { - styledPlayerControlView.removeCallbacks(hideControllerRunnable); - styledPlayerControlView.removeCallbacks(hideAllBarsRunnable); - styledPlayerControlView.removeCallbacks(hideMainBarRunnable); - styledPlayerControlView.removeCallbacks(hideProgressBarRunnable); + playerControlView.removeCallbacks(hideControllerRunnable); + playerControlView.removeCallbacks(hideAllBarsRunnable); + playerControlView.removeCallbacks(hideMainBarRunnable); + playerControlView.removeCallbacks(hideProgressBarRunnable); } public void onAttachedToWindow() { - styledPlayerControlView.addOnLayoutChangeListener(onLayoutChangeListener); + playerControlView.addOnLayoutChangeListener(onLayoutChangeListener); } public void onDetachedFromWindow() { - styledPlayerControlView.removeOnLayoutChangeListener(onLayoutChangeListener); + playerControlView.removeOnLayoutChangeListener(onLayoutChangeListener); } public boolean isFullyVisible() { - return uxState == UX_STATE_ALL_VISIBLE && styledPlayerControlView.isVisible(); + return uxState == UX_STATE_ALL_VISIBLE && playerControlView.isVisible(); } public void setShowButton(@Nullable View button, boolean showButton) { @@ -451,14 +450,14 @@ import java.util.List; int prevUxState = this.uxState; this.uxState = uxState; if (uxState == UX_STATE_NONE_VISIBLE) { - styledPlayerControlView.setVisibility(View.GONE); + playerControlView.setVisibility(View.GONE); } else if (prevUxState == UX_STATE_NONE_VISIBLE) { - styledPlayerControlView.setVisibility(View.VISIBLE); + playerControlView.setVisibility(View.VISIBLE); } // TODO(insun): Notify specific uxState. Currently reuses legacy visibility listener for API // compatibility. if (prevUxState != uxState) { - styledPlayerControlView.notifyOnVisibilityChange(); + playerControlView.notifyOnVisibilityChange(); } } @@ -550,7 +549,7 @@ import java.util.List; private void postDelayedRunnable(Runnable runnable, long interval) { if (interval >= 0) { - styledPlayerControlView.postDelayed(runnable, interval); + playerControlView.postDelayed(runnable, interval); } } @@ -571,13 +570,13 @@ import java.util.List; private boolean useMinimalMode() { int width = - styledPlayerControlView.getWidth() - - styledPlayerControlView.getPaddingLeft() - - styledPlayerControlView.getPaddingRight(); + playerControlView.getWidth() + - playerControlView.getPaddingLeft() + - playerControlView.getPaddingRight(); int height = - styledPlayerControlView.getHeight() - - styledPlayerControlView.getPaddingBottom() - - styledPlayerControlView.getPaddingTop(); + playerControlView.getHeight() + - playerControlView.getPaddingBottom() + - playerControlView.getPaddingTop(); int centerControlWidth = getWidthWithMargins(centerControls) @@ -607,7 +606,7 @@ import java.util.List; if (timeBar != null) { MarginLayoutParams timeBarParams = (MarginLayoutParams) timeBar.getLayoutParams(); int timeBarMarginBottom = - styledPlayerControlView + playerControlView .getResources() .getDimensionPixelSize(R.dimen.exo_styled_progress_margin_bottom); timeBarParams.bottomMargin = (isMinimalMode ? 0 : timeBarMarginBottom); @@ -646,9 +645,9 @@ import java.util.List; } int width = - styledPlayerControlView.getWidth() - - styledPlayerControlView.getPaddingLeft() - - styledPlayerControlView.getPaddingRight(); + playerControlView.getWidth() + - playerControlView.getPaddingLeft() + - playerControlView.getPaddingRight(); // Reset back to all controls being basic controls and the overflow not being needed. The last // child of extraControls is the overflow hide button, which shouldn't be moved back. From 15dc86382f17a24a3e881e52e31a810c1ea44b49 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 10 Jan 2022 11:47:29 +0000 Subject: [PATCH 06/28] Remove setTag from (Ss|Hls|Dash)MediaSource.Factory This method has been deprecated since 2.12.0 ([commit](https://github.com/google/ExoPlayer/commit/d1bbd3507a818e14be965c300938f9d51f8b7836)). Also remove DashMediaSource.Factory#setLivePresentationDelayMs(long, boolean), this method has been deprecated since 2.13.0 ([commit](https://github.com/google/ExoPlayer/commit/41b58d503ae6666cea4ded114afde9fb23a5e199)). PiperOrigin-RevId: 420719877 --- RELEASENOTES.md | 7 ++ .../source/dash/DashMediaSource.java | 59 --------------- .../source/dash/DashMediaSourceTest.java | 75 ------------------- .../exoplayer2/source/hls/HlsMediaSource.java | 14 ---- .../source/hls/HlsMediaSourceTest.java | 36 --------- .../source/smoothstreaming/SsMediaSource.java | 16 ---- .../smoothstreaming/SsMediaSourceTest.java | 66 ---------------- 7 files changed, 7 insertions(+), 266 deletions(-) delete mode 100644 library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSourceTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cd2c7b8293..1cd9050639 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -101,6 +101,13 @@ `MediaItem.Builder#setStreamKeys` instead. * Remove `MediaSourceFactory#createMediaSource(Uri)`. Use `MediaSourceFactory#createMediaSource(MediaItem)` instead. + * Remove `setTag` from `DashMediaSource`, `HlsMediaSource` and + `SsMediaSource`. Use `MediaItem.Builder#setTag` instead. + * Remove `DashMediaSource#setLivePresentationDelayMs(long, boolean)`. Use + `MediaItem.Builder#setLiveConfiguration` and + `MediaItem.LiveConfiguration.Builder#setTargetOffsetMs` to override the + manifest, or `DashMediaSource#setFallbackTargetLiveOffsetMs` to provide + a fallback value. ### 2.16.1 (2021-11-18) 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 32b8eac0e7..92f03e29f5 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 @@ -106,10 +106,8 @@ public final class DashMediaSource extends BaseMediaSource { private DrmSessionManagerProvider drmSessionManagerProvider; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; - private long targetLiveOffsetOverrideMs; private long fallbackTargetLiveOffsetMs; @Nullable private ParsingLoadable.Parser manifestParser; - @Nullable private Object tag; /** * Creates a new factory for {@link DashMediaSource}s. @@ -137,21 +135,10 @@ public final class DashMediaSource extends BaseMediaSource { this.manifestDataSourceFactory = manifestDataSourceFactory; drmSessionManagerProvider = new DefaultDrmSessionManagerProvider(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); - targetLiveOffsetOverrideMs = C.TIME_UNSET; fallbackTargetLiveOffsetMs = DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } - /** - * @deprecated Use {@link MediaItem.Builder#setTag(Object)} and {@link - * #createMediaSource(MediaItem)} instead. - */ - @Deprecated - public Factory setTag(@Nullable Object tag) { - this.tag = tag; - return this; - } - @Override public Factory setDrmSessionManagerProvider( @Nullable DrmSessionManagerProvider drmSessionManagerProvider) { @@ -178,21 +165,6 @@ public final class DashMediaSource extends BaseMediaSource { return this; } - /** - * @deprecated Use {@link MediaItem.Builder#setLiveConfiguration(MediaItem.LiveConfiguration)} - * and {@link MediaItem.LiveConfiguration.Builder#setTargetOffsetMs(long)} to override the - * manifest, or {@link #setFallbackTargetLiveOffsetMs(long)} to provide a fallback value. - */ - @Deprecated - public Factory setLivePresentationDelayMs( - long livePresentationDelayMs, boolean overridesManifest) { - targetLiveOffsetOverrideMs = overridesManifest ? livePresentationDelayMs : C.TIME_UNSET; - if (!overridesManifest) { - setFallbackTargetLiveOffsetMs(livePresentationDelayMs); - } - return this; - } - /** * Sets the target {@link Player#getCurrentLiveOffset() offset for live streams} that is used if * no value is defined in the {@link MediaItem} or the manifest. @@ -253,7 +225,6 @@ public final class DashMediaSource extends BaseMediaSource { .setUri(Uri.EMPTY) .setMediaId(DEFAULT_MEDIA_ID) .setMimeType(MimeTypes.APPLICATION_MPD) - .setTag(tag) .build()); } @@ -273,17 +244,6 @@ public final class DashMediaSource extends BaseMediaSource { if (mediaItem.localConfiguration == null) { mediaItemBuilder.setUri(Uri.EMPTY); } - if (mediaItem.localConfiguration == null || mediaItem.localConfiguration.tag == null) { - mediaItemBuilder.setTag(tag); - } - if (mediaItem.liveConfiguration.targetOffsetMs == C.TIME_UNSET) { - mediaItemBuilder.setLiveConfiguration( - mediaItem - .liveConfiguration - .buildUpon() - .setTargetOffsetMs(targetLiveOffsetOverrideMs) - .build()); - } mediaItem = mediaItemBuilder.build(); return new DashMediaSource( mediaItem, @@ -316,25 +276,6 @@ public final class DashMediaSource extends BaseMediaSource { manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys); } - boolean needsTag = mediaItem.localConfiguration.tag == null && tag != null; - boolean needsTargetLiveOffset = - mediaItem.liveConfiguration.targetOffsetMs == C.TIME_UNSET - && targetLiveOffsetOverrideMs != C.TIME_UNSET; - if (needsTag || needsTargetLiveOffset) { - MediaItem.Builder builder = mediaItem.buildUpon(); - if (needsTag) { - builder.setTag(tag); - } - if (needsTargetLiveOffset) { - builder.setLiveConfiguration( - mediaItem - .liveConfiguration - .buildUpon() - .setTargetOffsetMs(targetLiveOffsetOverrideMs) - .build()); - } - mediaItem = builder.build(); - } return new DashMediaSource( mediaItem, /* manifest= */ null, 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 f66ec8a7eb..bebcb074ca 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 @@ -99,40 +99,6 @@ public final class DashMediaSourceTest { } } - // Tests backwards compatibility - @SuppressWarnings("deprecation") - @Test - public void factorySetTag_nullMediaItemTag_setsMediaItemTag() { - Object tag = new Object(); - MediaItem mediaItem = MediaItem.fromUri("http://www.google.com"); - DashMediaSource.Factory factory = - new DashMediaSource.Factory(new FileDataSource.Factory()).setTag(tag); - - MediaItem dashMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(dashMediaItem.localConfiguration).isNotNull(); - assertThat(dashMediaItem.localConfiguration.uri).isEqualTo(mediaItem.localConfiguration.uri); - assertThat(dashMediaItem.localConfiguration.tag).isEqualTo(tag); - } - - // Tests backwards compatibility - @SuppressWarnings("deprecation") - @Test - public void factorySetTag_nonNullMediaItemTag_doesNotOverrideMediaItemTag() { - Object factoryTag = new Object(); - Object mediaItemTag = new Object(); - MediaItem mediaItem = - new MediaItem.Builder().setUri("http://www.google.com").setTag(mediaItemTag).build(); - DashMediaSource.Factory factory = - new DashMediaSource.Factory(new FileDataSource.Factory()).setTag(factoryTag); - - MediaItem dashMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(dashMediaItem.localConfiguration).isNotNull(); - assertThat(dashMediaItem.localConfiguration.uri).isEqualTo(mediaItem.localConfiguration.uri); - assertThat(dashMediaItem.localConfiguration.tag).isEqualTo(mediaItemTag); - } - @Test public void replaceManifestUri_doesNotChangeMediaItem() { DashMediaSource.Factory factory = new DashMediaSource.Factory(new FileDataSource.Factory()); @@ -161,47 +127,6 @@ public final class DashMediaSourceTest { assertThat(dashMediaItem.liveConfiguration.targetOffsetMs).isEqualTo(2L); } - @Test - public void factorySetLivePresentationDelayMs_withMediaLiveTargetOffset_usesMediaOffset() { - MediaItem mediaItem = - new MediaItem.Builder() - .setUri(Uri.EMPTY) - .setLiveConfiguration( - new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(2L).build()) - .build(); - DashMediaSource.Factory factory = - new DashMediaSource.Factory(new FileDataSource.Factory()) - .setLivePresentationDelayMs(1234L, /* overridesManifest= */ true); - - MediaItem dashMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(dashMediaItem.liveConfiguration.targetOffsetMs).isEqualTo(2L); - } - - @Test - public void factorySetLivePresentationDelayMs_overridingManifest_mixedIntoMediaItem() { - MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).build(); - DashMediaSource.Factory factory = - new DashMediaSource.Factory(new FileDataSource.Factory()) - .setLivePresentationDelayMs(2000L, /* overridesManifest= */ true); - - MediaItem dashMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(dashMediaItem.liveConfiguration.targetOffsetMs).isEqualTo(2000L); - } - - @Test - public void factorySetLivePresentationDelayMs_notOverridingManifest_unsetInMediaItem() { - MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).build(); - DashMediaSource.Factory factory = - new DashMediaSource.Factory(new FileDataSource.Factory()) - .setLivePresentationDelayMs(2000L, /* overridesManifest= */ false); - - MediaItem dashMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(dashMediaItem.liveConfiguration.targetOffsetMs).isEqualTo(C.TIME_UNSET); - } - @Test public void factorySetFallbackTargetLiveOffsetMs_doesNotChangeMediaItem() { DashMediaSource.Factory 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 70fbb52c61..b6028b7649 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 @@ -106,7 +106,6 @@ public final class HlsMediaSource extends BaseMediaSource private boolean allowChunklessPreparation; private @MetadataType int metadataType; private boolean useSessionKeys; - @Nullable private Object tag; private long elapsedRealTimeOffsetMs; /** @@ -139,16 +138,6 @@ public final class HlsMediaSource extends BaseMediaSource allowChunklessPreparation = true; } - /** - * @deprecated Use {@link MediaItem.Builder#setTag(Object)} and {@link - * #createMediaSource(MediaItem)} instead. - */ - @Deprecated - public Factory setTag(@Nullable Object tag) { - this.tag = tag; - return this; - } - /** * Sets the factory for {@link Extractor}s for the segments. The default value is {@link * HlsExtractorFactory#DEFAULT}. @@ -322,9 +311,6 @@ public final class HlsMediaSource extends BaseMediaSource new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys); } - if (mediaItem.localConfiguration.tag == null && tag != null) { - mediaItem = mediaItem.buildUpon().setTag(tag).build(); - } return new HlsMediaSource( mediaItem, hlsDataSourceFactory, 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 81c9709c09..9555fb73b4 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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source.hls; import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.mock; import android.net.Uri; import android.os.SystemClock; @@ -31,7 +30,6 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; -import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Util; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -46,40 +44,6 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class HlsMediaSourceTest { - // Tests backwards compatibility - @SuppressWarnings("deprecation") - @Test - public void factorySetTag_nullMediaItemTag_setsMediaItemTag() { - Object tag = new Object(); - MediaItem mediaItem = MediaItem.fromUri("http://www.google.com"); - HlsMediaSource.Factory factory = - new HlsMediaSource.Factory(mock(DataSource.Factory.class)).setTag(tag); - - MediaItem hlsMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(hlsMediaItem.localConfiguration).isNotNull(); - assertThat(hlsMediaItem.localConfiguration.uri).isEqualTo(mediaItem.localConfiguration.uri); - assertThat(hlsMediaItem.localConfiguration.tag).isEqualTo(tag); - } - - // Tests backwards compatibility - @SuppressWarnings("deprecation") - @Test - public void factorySetTag_nonNullMediaItemTag_doesNotOverrideMediaItemTag() { - Object factoryTag = new Object(); - Object mediaItemTag = new Object(); - MediaItem mediaItem = - new MediaItem.Builder().setUri("http://www.google.com").setTag(mediaItemTag).build(); - HlsMediaSource.Factory factory = - new HlsMediaSource.Factory(mock(DataSource.Factory.class)).setTag(factoryTag); - - MediaItem hlsMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(hlsMediaItem.localConfiguration).isNotNull(); - assertThat(hlsMediaItem.localConfiguration.uri).isEqualTo(mediaItem.localConfiguration.uri); - assertThat(hlsMediaItem.localConfiguration.tag).isEqualTo(mediaItemTag); - } - @Test public void loadLivePlaylist_noTargetLiveOffsetDefined_fallbackToThreeTargetDuration() throws TimeoutException, ParserException { 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 977bac524a..e94f97af7d 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 @@ -87,7 +87,6 @@ public final class SsMediaSource extends BaseMediaSource private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long livePresentationDelayMs; @Nullable private ParsingLoadable.Parser manifestParser; - @Nullable private Object tag; /** * Creates a new factory for {@link SsMediaSource}s. @@ -119,16 +118,6 @@ public final class SsMediaSource extends BaseMediaSource compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } - /** - * @deprecated Use {@link MediaItem.Builder#setTag(Object)} and {@link - * #createMediaSource(MediaItem)} instead. - */ - @Deprecated - public Factory setTag(@Nullable Object tag) { - this.tag = tag; - return this; - } - /** * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. @@ -231,13 +220,11 @@ public final class SsMediaSource extends BaseMediaSource manifest = manifest.copy(streamKeys); } boolean hasUri = mediaItem.localConfiguration != null; - boolean hasTag = hasUri && mediaItem.localConfiguration.tag != null; mediaItem = mediaItem .buildUpon() .setMimeType(MimeTypes.APPLICATION_SS) .setUri(hasUri ? mediaItem.localConfiguration.uri : Uri.EMPTY) - .setTag(hasTag ? mediaItem.localConfiguration.tag : tag) .build(); return new SsMediaSource( mediaItem, @@ -270,9 +257,6 @@ public final class SsMediaSource extends BaseMediaSource manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys); } - if (mediaItem.localConfiguration.tag == null && tag != null) { - mediaItem = mediaItem.buildUpon().setTag(tag).build(); - } return new SsMediaSource( mediaItem, /* manifest= */ null, 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 deleted file mode 100644 index 8bda74b25a..0000000000 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSourceTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 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.source.smoothstreaming; - -import static com.google.android.exoplayer2.util.Util.castNonNull; -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.upstream.FileDataSource; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit tests for {@link SsMediaSource}. */ -@RunWith(AndroidJUnit4.class) -public class SsMediaSourceTest { - - // Tests backwards compatibility - @SuppressWarnings("deprecation") - @Test - public void factorySetTag_nullMediaItemTag_setsMediaItemTag() { - Object tag = new Object(); - MediaItem mediaItem = MediaItem.fromUri("http://www.google.com"); - SsMediaSource.Factory factory = - new SsMediaSource.Factory(new FileDataSource.Factory()).setTag(tag); - - MediaItem ssMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(ssMediaItem.localConfiguration).isNotNull(); - assertThat(ssMediaItem.localConfiguration.uri) - .isEqualTo(castNonNull(mediaItem.localConfiguration).uri); - assertThat(ssMediaItem.localConfiguration.tag).isEqualTo(tag); - } - - // Tests backwards compatibility - @SuppressWarnings("deprecation") - @Test - public void factorySetTag_nonNullMediaItemTag_doesNotOverrideMediaItemTag() { - Object factoryTag = new Object(); - Object mediaItemTag = new Object(); - MediaItem mediaItem = - new MediaItem.Builder().setUri("http://www.google.com").setTag(mediaItemTag).build(); - SsMediaSource.Factory factory = - new SsMediaSource.Factory(new FileDataSource.Factory()).setTag(factoryTag); - - MediaItem ssMediaItem = factory.createMediaSource(mediaItem).getMediaItem(); - - assertThat(ssMediaItem.localConfiguration).isNotNull(); - assertThat(ssMediaItem.localConfiguration.uri) - .isEqualTo(castNonNull(mediaItem.localConfiguration).uri); - assertThat(ssMediaItem.localConfiguration.tag).isEqualTo(mediaItemTag); - } -} From 8c89c9c688d978427a34499cf5b7724bc64ecc8d Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 10 Jan 2022 13:49:53 +0000 Subject: [PATCH 07/28] Require playback to be stuck for a minimum period before failing PiperOrigin-RevId: 420738165 --- .../exoplayer2/ExoPlayerImplInternal.java | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 76471b5c7e..f1082e7533 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -167,6 +167,13 @@ import java.util.concurrent.atomic.AtomicBoolean; * this does not matter for now. */ private static final long MIN_RENDERER_SLEEP_DURATION_MS = 2000; + /** + * Duration for which the player needs to appear stuck before the playback is failed on the + * assumption that no further progress will be made. To appear stuck, the player's renderers must + * not be ready, there must be more media available to load, and the LoadControl must be refusing + * to load it. + */ + private static final long PLAYBACK_STUCK_AFTER_MS = 4000; private final Renderer[] renderers; private final Set renderersToReset; @@ -206,15 +213,14 @@ import java.util.concurrent.atomic.AtomicBoolean; private boolean foregroundMode; private boolean requestForRendererSleep; private boolean offloadSchedulingEnabled; - private int enabledRendererCount; @Nullable private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; private int nextPendingMessageIndexHint; private boolean deliverPendingMessageAtStartPositionRequired; @Nullable private ExoPlaybackException pendingRecoverableRendererError; - private long setForegroundModeTimeoutMs; + private long playbackMaybeBecameStuckAtMs; public ExoPlayerImplInternal( Renderer[] renderers, @@ -248,6 +254,7 @@ import java.util.concurrent.atomic.AtomicBoolean; this.pauseAtEndOfWindow = pauseAtEndOfWindow; this.clock = clock; + playbackMaybeBecameStuckAtMs = C.TIME_UNSET; backBufferDurationUs = loadControl.getBackBufferDurationUs(); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); @@ -668,6 +675,9 @@ import java.util.concurrent.atomic.AtomicBoolean; private void setState(int state) { if (playbackInfo.playbackState != state) { + if (state != Player.STATE_BUFFERING) { + playbackMaybeBecameStuckAtMs = C.TIME_UNSET; + } playbackInfo = playbackInfo.copyWithPlaybackState(state); } } @@ -1038,6 +1048,7 @@ import java.util.concurrent.atomic.AtomicBoolean; stopRenderers(); } + boolean playbackMaybeStuck = false; if (playbackInfo.playbackState == Player.STATE_BUFFERING) { for (int i = 0; i < renderers.length; i++) { if (isRendererEnabled(renderers[i]) @@ -1048,12 +1059,24 @@ import java.util.concurrent.atomic.AtomicBoolean; if (!playbackInfo.isLoading && playbackInfo.totalBufferedDurationUs < 500_000 && isLoadingPossible()) { - // Throw if the LoadControl prevents loading even if the buffer is empty or almost empty. We - // can't compare against 0 to account for small differences between the renderer position - // and buffered position in the media at the point where playback gets stuck. - throw new IllegalStateException("Playback stuck buffering and not loading"); + // The renderers are not ready, there is more media available to load, and the LoadControl + // is refusing to load it (indicated by !playbackInfo.isLoading). This could be because the + // renderers are still transitioning to their ready states, but it could also indicate a + // stuck playback. The playbackInfo.totalBufferedDurationUs check further isolates the + // cause to a lack of media for the renderers to consume, to avoid classifying playbacks as + // stuck when they're waiting for other reasons (in particular, loading DRM keys). + playbackMaybeStuck = true; } } + + if (!playbackMaybeStuck) { + playbackMaybeBecameStuckAtMs = C.TIME_UNSET; + } else if (playbackMaybeBecameStuckAtMs == C.TIME_UNSET) { + playbackMaybeBecameStuckAtMs = clock.elapsedRealtime(); + } else if (clock.elapsedRealtime() - playbackMaybeBecameStuckAtMs >= PLAYBACK_STUCK_AFTER_MS) { + throw new IllegalStateException("Playback stuck buffering and not loading"); + } + if (offloadSchedulingEnabled != playbackInfo.offloadSchedulingEnabled) { playbackInfo = playbackInfo.copyWithOffloadSchedulingEnabled(offloadSchedulingEnabled); } From c3b470f308487d28d132fa6e23be56c37e5ce91f Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 10 Jan 2022 22:35:28 +0000 Subject: [PATCH 08/28] Remove most allocations in SampleQueue.release SampleQueues may be released in the context of a finally block after an out of memory error. Allocating in that scenario can throw yet a new OutOfMemoryError. By safely releasing SampleQueue memory, we increase the possibility of handling the error gracefully. PiperOrigin-RevId: 420859022 --- .../exoplayer2/source/SampleDataQueue.java | 38 ++++++++++++------- .../exoplayer2/upstream/Allocator.java | 25 +++++++++++- .../exoplayer2/upstream/DefaultAllocator.java | 11 ++++++ 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java index d9e7ca95d2..5cf5660b02 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.source.SampleQueue.SampleExtrasHolder; import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataReader; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; @@ -79,6 +80,7 @@ import java.util.Arrays; * discarded, or 0 if the queue is now empty. */ public void discardUpstreamSampleBytes(long totalBytesWritten) { + Assertions.checkArgument(totalBytesWritten <= this.totalBytesWritten); this.totalBytesWritten = totalBytesWritten; if (this.totalBytesWritten == 0 || this.totalBytesWritten == firstAllocationNode.startPosition) { @@ -92,8 +94,8 @@ import java.util.Arrays; while (this.totalBytesWritten > lastNodeToKeep.endPosition) { lastNodeToKeep = lastNodeToKeep.next; } - // Discard all subsequent nodes. - AllocationNode firstNodeToDiscard = lastNodeToKeep.next; + // Discard all subsequent nodes. lastNodeToKeep is initialized, therefore next cannot be null. + AllocationNode firstNodeToDiscard = Assertions.checkNotNull(lastNodeToKeep.next); clearAllocationNodes(firstNodeToDiscard); // Reset the successor of the last node to be an uninitialized node. lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength); @@ -213,17 +215,8 @@ import java.util.Arrays; // Bulk release allocations for performance (it's significantly faster when using // DefaultAllocator because the allocator's lock only needs to be acquired and released once) // [Internal: See b/29542039]. - int allocationCount = - (writeAllocationNode.allocation != null ? 1 : 0) - + ((int) (writeAllocationNode.startPosition - fromNode.startPosition) - / allocationLength); - Allocation[] allocationsToRelease = new Allocation[allocationCount]; - AllocationNode currentNode = fromNode; - for (int i = 0; i < allocationsToRelease.length; i++) { - allocationsToRelease[i] = currentNode.allocation; - currentNode = currentNode.clear(); - } - allocator.release(allocationsToRelease); + allocator.release(fromNode); + fromNode.clear(); } /** @@ -466,7 +459,7 @@ import java.util.Arrays; } /** A node in a linked list of {@link Allocation}s held by the output. */ - private static final class AllocationNode { + private static final class AllocationNode implements Allocator.AllocationNode { /** The absolute position of the start of the data (inclusive). */ public final long startPosition; @@ -525,5 +518,22 @@ import java.util.Arrays; next = null; return temp; } + + // AllocationChainNode implementation. + + @Override + public Allocation getAllocation() { + return Assertions.checkNotNull(allocation); + } + + @Override + @Nullable + public Allocator.AllocationNode next() { + if (next == null || next.allocation == null) { + return null; + } else { + return next; + } + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocator.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocator.java index 6b9ddcc1da..22d132dfed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocator.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocator.java @@ -15,9 +15,22 @@ */ package com.google.android.exoplayer2.upstream; +import androidx.annotation.Nullable; + /** A source of allocations. */ public interface Allocator { + /** A node in a chain of {@link Allocation Allocations}. */ + interface AllocationNode { + + /** Returns the {@link Allocation} associated to this chain node. */ + Allocation getAllocation(); + + /** Returns the next chain node, or {@code null} if this is the last node in the chain. */ + @Nullable + AllocationNode next(); + } + /** * Obtain an {@link Allocation}. * @@ -36,15 +49,23 @@ public interface Allocator { void release(Allocation allocation); /** - * Releases an array of {@link Allocation}s back to the allocator. + * Releases an array of {@link Allocation Allocations} back to the allocator. * * @param allocations The array of {@link Allocation}s being released. */ void release(Allocation[] allocations); + /** + * Releases all {@link Allocation Allocations} in the chain starting at the given {@link + * AllocationNode}. + * + *

Implementations must not make memory allocations. + */ + void release(AllocationNode allocationNode); + /** * Hints to the allocator that it should make a best effort to release any excess {@link - * Allocation}s. + * Allocation Allocations}. */ void trim(); 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 47a77beb38..7797883d20 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 @@ -128,6 +128,17 @@ public final class DefaultAllocator implements Allocator { notifyAll(); } + @Override + public synchronized void release(@Nullable AllocationNode allocationNode) { + while (allocationNode != null) { + availableAllocations[availableCount++] = allocationNode.getAllocation(); + allocatedCount--; + allocationNode = allocationNode.next(); + } + // Wake up threads waiting for the allocated size to drop. + notifyAll(); + } + @Override public synchronized void trim() { int targetAllocationCount = Util.ceilDivide(targetBufferSize, individualAllocationSize); From 607ef989fbe0bf966942eb5a2bd01a3eacd708bb Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 11 Jan 2022 08:37:46 +0000 Subject: [PATCH 09/28] Fix decoder fallback logic for Dolby Atmos and Dolby Vision. The media codec renderers have fallback logic in getDecoderInfos to assume that E-AC3 decoders can handle the 2D version of E-AC3-JOC and that H264/H265 decoders can handle some base layer of Dolby Vision content. Both fallbacks are useful if there is no decoder for the enhanced Dolby formats. Both fallbacks are not applied during track selection at the moment because the separate MediaCodecInfo.isCodecSupported method verifies that the mime type corresponding to format.codecs is the same as the decoder mime type (which isn't true for the fallback case). To fix the fallback logic, we can just completely remove this additional check because it's not needed in the context of this method that is only called after we already established that the decoder can handle the format.sampleMimeType. In addition, we need to map the Dolby Vision profiles to the equivalent H264/H265 profile to make the codec profile comparison sensible again. PiperOrigin-RevId: 420959104 --- RELEASENOTES.md | 2 + .../audio/MediaCodecAudioRenderer.java | 25 +++-- .../exoplayer2/mediacodec/MediaCodecInfo.java | 43 +++++--- .../exoplayer2/mediacodec/MediaCodecUtil.java | 42 ++++++- .../video/MediaCodecVideoRenderer.java | 42 ++----- .../audio/MediaCodecAudioRendererTest.java | 48 +++++++- .../video/MediaCodecVideoRendererTest.java | 103 ++++++++++++++++++ 7 files changed, 240 insertions(+), 65 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1cd9050639..a55d2fc1d3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -26,6 +26,8 @@ live edge ((#9784)[https://github.com/google/ExoPlayer/issues/9784]). * Fix Maven dependency resolution ((#8353)[https://github.com/google/ExoPlayer/issues/8353]). + * Fix decoder fallback logic for Dolby Atmos (E-AC3-JOC) and Dolby Vision + to use a compatible base decoder (E-AC3 or H264/H265) if needed. * 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/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 4a2b5314d2..42a5945f00 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 @@ -56,9 +56,8 @@ import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MediaFormatUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableList; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -381,27 +380,29 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media throws DecoderQueryException { @Nullable String mimeType = format.sampleMimeType; if (mimeType == null) { - return Collections.emptyList(); + return ImmutableList.of(); } if (audioSink.supportsFormat(format)) { // The format is supported directly, so a codec is only needed for decryption. @Nullable MediaCodecInfo codecInfo = MediaCodecUtil.getDecryptOnlyDecoderInfo(); if (codecInfo != null) { - return Collections.singletonList(codecInfo); + return ImmutableList.of(codecInfo); } } List decoderInfos = mediaCodecSelector.getDecoderInfos( mimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); - 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); - decoderInfosWithEac3.addAll( - mediaCodecSelector.getDecoderInfos( - MimeTypes.AUDIO_E_AC3, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false)); - decoderInfos = decoderInfosWithEac3; + @Nullable String alternativeMimeType = MediaCodecUtil.getAlternativeCodecMimeType(format); + if (alternativeMimeType == null) { + return ImmutableList.copyOf(decoderInfos); } - return Collections.unmodifiableList(decoderInfos); + List alternativeDecoderInfos = + mediaCodecSelector.getDecoderInfos( + alternativeMimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); + return ImmutableList.builder() + .addAll(decoderInfos) + .addAll(alternativeDecoderInfos) + .build(); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 6d4c11603a..5b1cb66095 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -241,7 +241,11 @@ public final class MediaCodecInfo { * @throws MediaCodecUtil.DecoderQueryException Thrown if an error occurs while querying decoders. */ public boolean isFormatSupported(Format format) throws MediaCodecUtil.DecoderQueryException { - if (!isCodecSupported(format)) { + if (!isSampleMimeTypeSupported(format)) { + return false; + } + + if (!isCodecProfileAndLevelSupported(format)) { return false; } @@ -268,25 +272,15 @@ public final class MediaCodecInfo { } } - /** - * Whether the decoder supports the codec of the given {@code format}. If there is insufficient - * information to decide, returns true. - * - * @param format The input media format. - * @return True if the codec of the given {@code format} is supported by the decoder. - */ - public boolean isCodecSupported(Format format) { - if (format.codecs == null || mimeType == null) { + private boolean isSampleMimeTypeSupported(Format format) { + return mimeType.equals(format.sampleMimeType) + || mimeType.equals(MediaCodecUtil.getAlternativeCodecMimeType(format)); + } + + private boolean isCodecProfileAndLevelSupported(Format format) { + if (format.codecs == null) { return true; } - String codecMimeType = MimeTypes.getMediaMimeType(format.codecs); - if (codecMimeType == null) { - return true; - } - if (!mimeType.equals(codecMimeType)) { - logNoSupport("codec.mime " + format.codecs + ", " + codecMimeType); - return false; - } Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); if (codecProfileAndLevel == null) { // If we don't know any better, we assume that the profile and level are supported. @@ -294,6 +288,19 @@ public final class MediaCodecInfo { } int profile = codecProfileAndLevel.first; int level = codecProfileAndLevel.second; + if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { + // If this codec is H264 or H265, we only support the Dolby Vision base layer and need to map + // the Dolby Vision profile to the corresponding base layer profile. Also assume all levels of + // this base layer profile are supported. + if (MimeTypes.VIDEO_H264.equals(mimeType)) { + profile = CodecProfileLevel.AVCProfileHigh; + level = 0; + } else if (MimeTypes.VIDEO_H265.equals(mimeType)) { + profile = CodecProfileLevel.HEVCProfileMain10; + level = 0; + } + } + if (!isVideo && profile != CodecProfileLevel.AACObjectXHE) { // Some devices/builds underreport audio capabilities, so assume support except for xHE-AAC // which may not be widely supported. See https://github.com/google/ExoPlayer/issues/5145. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index c60836fc2b..4f1443c7d1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.ColorInfo; import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -181,9 +182,9 @@ public final class MediaCodecUtil { } } applyWorkarounds(mimeType, decoderInfos); - List unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos); - decoderInfosCache.put(key, unmodifiableDecoderInfos); - return unmodifiableDecoderInfos; + ImmutableList immutableDecoderInfos = ImmutableList.copyOf(decoderInfos); + decoderInfosCache.put(key, immutableDecoderInfos); + return immutableDecoderInfos; } /** @@ -266,6 +267,41 @@ public final class MediaCodecUtil { } } + /** + * Returns an alternative codec MIME type (besides the default {@link Format#sampleMimeType}) that + * can be used to decode samples of the provided {@link Format}. + * + * @param format The media format. + * @return An alternative MIME type of a codec that be used decode samples of the provided {@code + * Format} (besides the default {@link Format#sampleMimeType}), or null if no such alternative + * exists. + */ + @Nullable + public static String getAlternativeCodecMimeType(Format format) { + if (MimeTypes.AUDIO_E_AC3_JOC.equals(format.sampleMimeType)) { + // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D. + return MimeTypes.AUDIO_E_AC3; + } + if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { + // H.264/AVC or H.265/HEVC decoders can decode the base layer of some DV profiles. This can't + // be done for profile CodecProfileLevel.DolbyVisionProfileDvheStn and profile + // CodecProfileLevel.DolbyVisionProfileDvheDtb because the first one is not backward + // compatible and the second one is deprecated and is not always backward compatible. + @Nullable + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); + if (codecProfileAndLevel != null) { + int profile = codecProfileAndLevel.first; + if (profile == CodecProfileLevel.DolbyVisionProfileDvheDtr + || profile == CodecProfileLevel.DolbyVisionProfileDvheSt) { + return MimeTypes.VIDEO_H265; + } else if (profile == CodecProfileLevel.DolbyVisionProfileDvavSe) { + return MimeTypes.VIDEO_H264; + } + } + } + return null; + } + // Internal methods. /** 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 c25d915b26..7f53747ea3 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 @@ -67,7 +67,6 @@ 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; /** @@ -462,41 +461,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { throws DecoderQueryException { @Nullable String mimeType = format.sampleMimeType; if (mimeType == null) { - return Collections.emptyList(); + return ImmutableList.of(); } List decoderInfos = mediaCodecSelector.getDecoderInfos( mimeType, requiresSecureDecoder, requiresTunnelingDecoder); - 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 - // CodecProfileLevel.DolbyVisionProfileDvheDtb because the first one is not backward - // compatible and the second one is deprecated and is not always backward compatible. - @Nullable - Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); - if (codecProfileAndLevel != null) { - List fallbackDecoderInfos; - int profile = codecProfileAndLevel.first; - if (profile == CodecProfileLevel.DolbyVisionProfileDvheDtr - || profile == CodecProfileLevel.DolbyVisionProfileDvheSt) { - fallbackDecoderInfos = - mediaCodecSelector.getDecoderInfos( - MimeTypes.VIDEO_H265, requiresSecureDecoder, requiresTunnelingDecoder); - } else if (profile == CodecProfileLevel.DolbyVisionProfileDvavSe) { - fallbackDecoderInfos = - mediaCodecSelector.getDecoderInfos( - MimeTypes.VIDEO_H264, requiresSecureDecoder, requiresTunnelingDecoder); - } else { - fallbackDecoderInfos = ImmutableList.of(); - } - decoderInfos = - ImmutableList.builder() - .addAll(decoderInfos) - .addAll(fallbackDecoderInfos) - .build(); - } + @Nullable String alternativeMimeType = MediaCodecUtil.getAlternativeCodecMimeType(format); + if (alternativeMimeType == null) { + return ImmutableList.copyOf(decoderInfos); } - return Collections.unmodifiableList(decoderInfos); + List alternativeDecoderInfos = + mediaCodecSelector.getDecoderInfos( + alternativeMimeType, requiresSecureDecoder, requiresTunnelingDecoder); + return ImmutableList.builder() + .addAll(decoderInfos) + .addAll(alternativeDecoderInfos) + .build(); } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java index 54e5511035..8571040181 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.audio; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.format; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -38,6 +39,8 @@ 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.RendererCapabilities.Capabilities; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.drm.DrmSessionEventListener; @@ -84,8 +87,14 @@ public class MediaCodecAudioRendererTest { // audioSink isEnded can always be true because the MediaCodecAudioRenderer isEnded = // super.isEnded && audioSink.isEnded. when(audioSink.isEnded()).thenReturn(true); - when(audioSink.handleBuffer(any(), anyLong(), anyInt())).thenReturn(true); + when(audioSink.supportsFormat(any())) + .thenAnswer( + invocation -> { + Format format = invocation.getArgument(/* index= */ 0, Format.class); + return MimeTypes.AUDIO_RAW.equals(format.sampleMimeType) + && format.pcmEncoding == C.ENCODING_PCM_16BIT; + }); mediaCodecSelector = (mimeType, requiresSecureDecoder, requiresTunnelingDecoder) -> @@ -315,6 +324,43 @@ public class MediaCodecAudioRendererTest { verify(audioRendererEventListener).onAudioSinkError(error); } + @Test + public void supportsFormat_withEac3JocMediaAndEac3Decoder_returnsTrue() throws Exception { + Format mediaFormat = + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_E_AC3_JOC) + .setCodecs(MimeTypes.CODEC_E_AC3_JOC) + .build(); + MediaCodecSelector mediaCodecSelector = + (mimeType, requiresSecureDecoder, requiresTunnelingDecoder) -> + !mimeType.equals(MimeTypes.AUDIO_E_AC3) + ? ImmutableList.of() + : ImmutableList.of( + MediaCodecInfo.newInstance( + /* name= */ "eac3-codec", + /* mimeType= */ mimeType, + /* codecMimeType= */ mimeType, + /* capabilities= */ null, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false)); + MediaCodecAudioRenderer renderer = + new MediaCodecAudioRenderer( + ApplicationProvider.getApplicationContext(), + mediaCodecSelector, + /* enableDecoderFallback= */ false, + /* eventHandler= */ new Handler(Looper.getMainLooper()), + audioRendererEventListener, + audioSink); + renderer.init(/* index= */ 0, PlayerId.UNSET); + + @Capabilities int capabilities = renderer.supportsFormat(mediaFormat); + + assertThat(RendererCapabilities.getFormatSupport(capabilities)).isEqualTo(C.FORMAT_HANDLED); + } + private static Format getAudioSinkFormat(Format inputFormat) { return new Format.Builder() .setSampleMimeType(MimeTypes.AUDIO_RAW) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java index 58b9424bb6..08816add89 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java @@ -27,6 +27,8 @@ import static org.mockito.Mockito.verify; import static org.robolectric.Shadows.shadowOf; import android.graphics.SurfaceTexture; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaFormat; import android.os.Handler; import android.os.Looper; @@ -39,7 +41,9 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; import com.google.android.exoplayer2.RendererConfiguration; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; @@ -506,4 +510,103 @@ public class MediaCodecVideoRendererTest { verify(eventListener, times(2)) .onRenderedFirstFrame(eq(surface), /* renderTimeMs= */ anyLong()); } + + @Test + public void supportsFormat_withDolbyVisionMedia_returnsTrueWhenFallbackToH265orH264Allowed() + throws Exception { + // Create Dolby media formats that could fall back to H265 or H264. + Format formatDvheDtrFallbackToH265 = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) + .setCodecs("dvhe.04.01") + .build(); + Format formatDvheStFallbackToH265 = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) + .setCodecs("dvhe.08.01") + .build(); + Format formatDvavSeFallbackToH264 = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) + .setCodecs("dvav.09.01") + .build(); + Format formatNoFallbackPossible = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) + .setCodecs("dvav.01.01") + .build(); + // Only provide H264 and H265 decoders with codec profiles needed for fallback. + MediaCodecSelector mediaCodecSelector = + (mimeType, requiresSecureDecoder, requiresTunnelingDecoder) -> { + switch (mimeType) { + case MimeTypes.VIDEO_H264: + CodecCapabilities capabilitiesH264 = new CodecCapabilities(); + capabilitiesH264.profileLevels = + new CodecProfileLevel[] {new CodecProfileLevel(), new CodecProfileLevel()}; + capabilitiesH264.profileLevels[0].profile = CodecProfileLevel.AVCProfileBaseline; + capabilitiesH264.profileLevels[0].level = CodecProfileLevel.AVCLevel42; + capabilitiesH264.profileLevels[1].profile = CodecProfileLevel.AVCProfileHigh; + capabilitiesH264.profileLevels[1].level = CodecProfileLevel.AVCLevel42; + return ImmutableList.of( + MediaCodecInfo.newInstance( + /* name= */ "h264-codec", + /* mimeType= */ mimeType, + /* codecMimeType= */ mimeType, + /* capabilities= */ capabilitiesH264, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false)); + case MimeTypes.VIDEO_H265: + CodecCapabilities capabilitiesH265 = new CodecCapabilities(); + capabilitiesH265.profileLevels = + new CodecProfileLevel[] {new CodecProfileLevel(), new CodecProfileLevel()}; + capabilitiesH265.profileLevels[0].profile = CodecProfileLevel.HEVCProfileMain; + capabilitiesH265.profileLevels[0].level = CodecProfileLevel.HEVCMainTierLevel41; + capabilitiesH265.profileLevels[1].profile = CodecProfileLevel.HEVCProfileMain10; + capabilitiesH265.profileLevels[1].level = CodecProfileLevel.HEVCHighTierLevel51; + return ImmutableList.of( + MediaCodecInfo.newInstance( + /* name= */ "h265-codec", + /* mimeType= */ mimeType, + /* codecMimeType= */ mimeType, + /* capabilities= */ capabilitiesH265, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false)); + default: + return ImmutableList.of(); + } + }; + MediaCodecVideoRenderer renderer = + new MediaCodecVideoRenderer( + ApplicationProvider.getApplicationContext(), + mediaCodecSelector, + /* allowedJoiningTimeMs= */ 0, + /* eventHandler= */ new Handler(testMainLooper), + /* eventListener= */ eventListener, + /* maxDroppedFramesToNotify= */ 1); + renderer.init(/* index= */ 0, PlayerId.UNSET); + + @Capabilities + int capabilitiesDvheDtrFallbackToH265 = renderer.supportsFormat(formatDvheDtrFallbackToH265); + @Capabilities + int capabilitiesDvheStFallbackToH265 = renderer.supportsFormat(formatDvheStFallbackToH265); + @Capabilities + int capabilitiesDvavSeFallbackToH264 = renderer.supportsFormat(formatDvavSeFallbackToH264); + @Capabilities + int capabilitiesNoFallbackPossible = renderer.supportsFormat(formatNoFallbackPossible); + + assertThat(RendererCapabilities.getFormatSupport(capabilitiesDvheDtrFallbackToH265)) + .isEqualTo(C.FORMAT_HANDLED); + assertThat(RendererCapabilities.getFormatSupport(capabilitiesDvheStFallbackToH265)) + .isEqualTo(C.FORMAT_HANDLED); + assertThat(RendererCapabilities.getFormatSupport(capabilitiesDvavSeFallbackToH264)) + .isEqualTo(C.FORMAT_HANDLED); + assertThat(RendererCapabilities.getFormatSupport(capabilitiesNoFallbackPossible)) + .isEqualTo(C.FORMAT_UNSUPPORTED_SUBTYPE); + } } From 831cfe2026453cc9b97aeeb4314325f8dc2d0efc Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 11 Jan 2022 14:20:12 +0000 Subject: [PATCH 10/28] Add Cronet keywords to SSL certificate troubleshooting entry We [recommend apps use Cronet](https://exoplayer.dev/network-stacks.html#choosing-a-network-stack) and the demo app uses it, so we should make it easy to look-up errors like this in our troubleshooting page. Issue: google/ExoPlayer#9851 #minor-release PiperOrigin-RevId: 421015537 --- docs/troubleshooting.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 80c922b615..0840c6b523 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -6,7 +6,7 @@ redirect_from: --- * [Fixing "Cleartext HTTP traffic not permitted" errors][] -* [Fixing "SSLHandshakeException" and "CertPathValidatorException" errors][] +* [Fixing "SSLHandshakeException", "CertPathValidatorException" and "ERR_CERT_AUTHORITY_INVALID" errors][] * [Why are some media files not seekable?][] * [Why is seeking inaccurate in some MP3 files?][] * [Why do some MPEG-TS files fail to play?][] @@ -43,11 +43,11 @@ The ExoPlayer demo app uses the default Network Security Configuration, and so does not allow cleartext HTTP traffic. You can enable it using the instructions above. -#### Fixing "SSLHandshakeException" and "CertPathValidatorException" errors #### +#### Fixing "SSLHandshakeException", "CertPathValidatorException" and "ERR_CERT_AUTHORITY_INVALID" errors #### -`SSLHandshakeException` and `CertPathValidatorException` both indicate a problem -with the server's SSL certificate. These errors are not ExoPlayer specific. -Please see +`SSLHandshakeException`, `CertPathValidatorException` and +`ERR_CERT_AUTHORITY_INVALID` all indicate a problem with the server's SSL +certificate. These errors are not ExoPlayer specific. Please see [Android's SSL documentation](https://developer.android.com/training/articles/security-ssl#CommonProblems) for more details. @@ -294,7 +294,7 @@ Android Player API](https://developers.google.com/youtube/android/player/) which is the official way to play YouTube videos on Android. [Fixing "Cleartext HTTP traffic not permitted" errors]: #fixing-cleartext-http-traffic-not-permitted-errors -[Fixing "SSLHandshakeException" and "CertPathValidatorException" errors]: #fixing-sslhandshakeexception-and-certpathvalidatorexception-errors +[Fixing "SSLHandshakeException", "CertPathValidatorException" and "ERR_CERT_AUTHORITY_INVALID" errors]: #fixing-sslhandshakeexception-certpathvalidatorexception-and-err_cert_authority_invalid-errors [What formats does ExoPlayer support?]: #what-formats-does-exoplayer-support [Why are some media files not seekable?]: #why-are-some-media-files-not-seekable [Why is seeking inaccurate in some MP3 files?]: #why-is-seeking-inaccurate-in-some-mp3-files From 6888a791f0a91dbb992dcd304976600359bfc64b Mon Sep 17 00:00:00 2001 From: hschlueter Date: Tue, 11 Jan 2022 16:03:40 +0000 Subject: [PATCH 11/28] Add error code and exception type for muxing failures. Exceptions thrown by MediaMuxer are converted MuxerExceptions and later to TransformationExceptions with ERROR_CODE_MUXING_FAILED. PiperOrigin-RevId: 421033721 --- .../transformer/FrameworkMuxer.java | 45 +++++++++++++++---- .../android/exoplayer2/transformer/Muxer.java | 27 +++++++++-- .../exoplayer2/transformer/MuxerWrapper.java | 12 +++-- .../transformer/TransformationException.java | 5 +++ .../exoplayer2/transformer/Transformer.java | 25 +++++++---- .../transformer/TransformerBaseRenderer.java | 6 ++- .../exoplayer2/transformer/TestMuxer.java | 7 +-- 7 files changed, 99 insertions(+), 28 deletions(-) diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameworkMuxer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameworkMuxer.java index 838e85b7c7..53599232ef 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameworkMuxer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameworkMuxer.java @@ -127,7 +127,7 @@ import java.nio.ByteBuffer; } @Override - public int addTrack(Format format) { + public int addTrack(Format format) throws MuxerException { String sampleMimeType = checkNotNull(format.sampleMimeType); MediaFormat mediaFormat; if (MimeTypes.isAudio(sampleMimeType)) { @@ -137,29 +137,56 @@ import java.nio.ByteBuffer; } else { mediaFormat = MediaFormat.createVideoFormat(castNonNull(sampleMimeType), format.width, format.height); - mediaMuxer.setOrientationHint(format.rotationDegrees); + try { + mediaMuxer.setOrientationHint(format.rotationDegrees); + } catch (RuntimeException e) { + throw new MuxerException( + "Failed to set orientation hint with rotationDegrees=" + format.rotationDegrees, e); + } } MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); - return mediaMuxer.addTrack(mediaFormat); + int trackIndex; + try { + trackIndex = mediaMuxer.addTrack(mediaFormat); + } catch (RuntimeException e) { + throw new MuxerException("Failed to add track with format=" + format, e); + } + return trackIndex; } @SuppressLint("WrongConstant") // C.BUFFER_FLAG_KEY_FRAME equals MediaCodec.BUFFER_FLAG_KEY_FRAME. @Override public void writeSampleData( - int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) { + int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) + throws MuxerException { if (!isStarted) { isStarted = true; - mediaMuxer.start(); + try { + mediaMuxer.start(); + } catch (RuntimeException e) { + throw new MuxerException("Failed to start the muxer", e); + } } int offset = data.position(); int size = data.limit() - offset; int flags = isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0; bufferInfo.set(offset, size, presentationTimeUs, flags); - mediaMuxer.writeSampleData(trackIndex, data, bufferInfo); + try { + mediaMuxer.writeSampleData(trackIndex, data, bufferInfo); + } catch (RuntimeException e) { + throw new MuxerException( + "Failed to write sample for trackIndex=" + + trackIndex + + ", presentationTimeUs=" + + presentationTimeUs + + ", size=" + + size, + e); + } } @Override - public void release(boolean forCancellation) { + public void release(boolean forCancellation) throws MuxerException { if (!isStarted) { mediaMuxer.release(); return; @@ -168,7 +195,7 @@ import java.nio.ByteBuffer; isStarted = false; try { mediaMuxer.stop(); - } catch (IllegalStateException e) { + } catch (RuntimeException e) { if (SDK_INT < 30) { // Set the muxer state to stopped even if mediaMuxer.stop() failed so that // mediaMuxer.release() doesn't attempt to stop the muxer and therefore doesn't throw the @@ -187,7 +214,7 @@ import java.nio.ByteBuffer; } // It doesn't matter that stopping the muxer throws if the transformation is being cancelled. if (!forCancellation) { - throw e; + throw new MuxerException("Failed to stop the muxer", e); } } finally { mediaMuxer.release(); diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Muxer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Muxer.java index 8ad039acab..fc6ae6d706 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Muxer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Muxer.java @@ -36,6 +36,19 @@ import java.nio.ByteBuffer; */ /* package */ interface Muxer { + /** Thrown when a muxing failure occurs. */ + /* package */ final class MuxerException extends Exception { + /** + * Creates an instance. + * + * @param message See {@link #getMessage()}. + * @param cause See {@link #getCause()}. + */ + public MuxerException(String message, Throwable cause) { + super(message, cause); + } + } + /** Factory for muxers. */ interface Factory { /** @@ -83,8 +96,11 @@ import java.nio.ByteBuffer; /** * Adds a track with the specified format, and returns its index (to be passed in subsequent calls * to {@link #writeSampleData(int, ByteBuffer, boolean, long)}). + * + * @param format The {@link Format} of the track. + * @throws MuxerException If the muxer encounters a problem while adding the track. */ - int addTrack(Format format); + int addTrack(Format format) throws MuxerException; /** * Writes the specified sample. @@ -93,15 +109,18 @@ import java.nio.ByteBuffer; * @param data Buffer containing the sample data to write to the container. * @param isKeyFrame Whether the sample is a key frame. * @param presentationTimeUs The presentation time of the sample in microseconds. + * @throws MuxerException If the muxer fails to start or an error occurs while writing the sample. */ - void writeSampleData( - int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs); + void writeSampleData(int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) + throws MuxerException; /** * Releases any resources associated with muxing. * * @param forCancellation Whether the reason for releasing the resources is the transformation * cancellation. + * @throws MuxerException If the muxer fails to stop or release resources and {@code + * forCancellation} is false. */ - void release(boolean forCancellation); + void release(boolean forCancellation) throws MuxerException; } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MuxerWrapper.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MuxerWrapper.java index 5eb1797d1e..67478587c5 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MuxerWrapper.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MuxerWrapper.java @@ -103,8 +103,10 @@ import java.nio.ByteBuffer; * @param format The {@link Format} to be added. * @throws IllegalStateException If the format is unsupported or if there is already a track * format of the same type (audio or video). + * @throws Muxer.MuxerException If the underlying muxer encounters a problem while adding the + * track. */ - public void addTrackFormat(Format format) { + public void addTrackFormat(Format format) throws Muxer.MuxerException { checkState(trackCount > 0, "All tracks should be registered before the formats are added."); checkState(trackFormatCount < trackCount, "All track formats have already been added."); @Nullable String sampleMimeType = format.sampleMimeType; @@ -138,9 +140,11 @@ import java.nio.ByteBuffer; * good interleaving. * @throws IllegalStateException If the muxer doesn't have any {@link #endTrack(int) non-ended} * track of the given track type. + * @throws Muxer.MuxerException If the underlying muxer fails to write the sample. */ public boolean writeSample( - @C.TrackType int trackType, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) { + @C.TrackType int trackType, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) + throws Muxer.MuxerException { int trackIndex = trackTypeToIndex.get(trackType, /* valueIfKeyNotFound= */ C.INDEX_UNSET); checkState( trackIndex != C.INDEX_UNSET, @@ -174,8 +178,10 @@ import java.nio.ByteBuffer; * * @param forCancellation Whether the reason for releasing the resources is the transformation * cancellation. + * @throws Muxer.MuxerException If the underlying muxer fails to stop and to release resources and + * {@code forCancellation} is false. */ - public void release(boolean forCancellation) { + public void release(boolean forCancellation) throws Muxer.MuxerException { isReady = false; muxer.release(forCancellation); } 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 7876c3f064..ce93e13c39 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 @@ -71,6 +71,8 @@ public final class TransformationException extends Exception { ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED, ERROR_CODE_GL_INIT_FAILED, ERROR_CODE_GL_PROCESSING_FAILED, + ERROR_CODE_MUXER_SAMPLE_MIME_TYPE_UNSUPPORTED, + ERROR_CODE_MUXING_FAILED, }) public @interface ErrorCode {} @@ -162,6 +164,8 @@ public final class TransformationException extends Exception { * TransformationRequest.Builder#setVideoMimeType(String)} to transcode to a supported MIME type. */ public static final int ERROR_CODE_MUXER_SAMPLE_MIME_TYPE_UNSUPPORTED = 6001; + /** Caused by a failure while muxing media samples. */ + public static final int ERROR_CODE_MUXING_FAILED = 6002; private static final ImmutableBiMap NAME_TO_ERROR_CODE = new ImmutableBiMap.Builder() @@ -186,6 +190,7 @@ public final class TransformationException extends Exception { .put( "ERROR_CODE_MUXER_SAMPLE_MIME_TYPE_UNSUPPORTED", ERROR_CODE_MUXER_SAMPLE_MIME_TYPE_UNSUPPORTED) + .put("ERROR_CODE_MUXING_FAILED", ERROR_CODE_MUXING_FAILED) .buildOrThrow(); /** Returns the {@code errorCode} for a given name. */ 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 522ccaa2c5..6005b0f972 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 @@ -670,7 +670,11 @@ public final class Transformer { * @throws IllegalStateException If this method is called from the wrong thread. */ public void cancel() { - releaseResources(/* forCancellation= */ true); + try { + releaseResources(/* forCancellation= */ true); + } catch (TransformationException impossible) { + throw new IllegalStateException(impossible); + } } /** @@ -679,17 +683,22 @@ public final class Transformer { * @param forCancellation Whether the reason for releasing the resources is the transformation * cancellation. * @throws IllegalStateException If this method is called from the wrong thread. - * @throws IllegalStateException If the muxer is in the wrong state and {@code forCancellation} is - * false. + * @throws TransformationException If the muxer is in the wrong state and {@code forCancellation} + * is false. */ - private void releaseResources(boolean forCancellation) { + private void releaseResources(boolean forCancellation) throws TransformationException { verifyApplicationThread(); if (player != null) { player.release(); player = null; } if (muxerWrapper != null) { - muxerWrapper.release(forCancellation); + try { + muxerWrapper.release(forCancellation); + } catch (Muxer.MuxerException e) { + throw TransformationException.createForMuxer( + e, TransformationException.ERROR_CODE_MUXING_FAILED); + } muxerWrapper = null; } progressState = PROGRESS_STATE_NO_TRANSFORMATION; @@ -824,9 +833,9 @@ public final class Transformer { @Nullable TransformationException resourceReleaseException = null; try { releaseResources(/* forCancellation= */ false); - } catch (IllegalStateException e) { - // TODO(internal b/209469847): Use a more specific error code when the IllegalStateException - // is caused by the muxer. + } catch (TransformationException e) { + resourceReleaseException = e; + } catch (RuntimeException e) { resourceReleaseException = TransformationException.createForUnexpected(e); } 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 e352ceec56..1e48226da6 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 @@ -97,6 +97,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; while (feedMuxerFromPipeline() || samplePipeline.processData() || feedPipelineFromInput()) {} } catch (TransformationException e) { throw wrapTransformationException(e); + } catch (Muxer.MuxerException e) { + throw wrapTransformationException( + TransformationException.createForMuxer( + e, TransformationException.ERROR_CODE_MUXING_FAILED)); } } @@ -145,7 +149,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * @return Whether it may be possible to write more data immediately by calling this method again. */ @RequiresNonNull("samplePipeline") - private boolean feedMuxerFromPipeline() { + private boolean feedMuxerFromPipeline() throws Muxer.MuxerException { if (!muxerWrapperTrackAdded) { @Nullable Format samplePipelineOutputFormat = samplePipeline.getOutputFormat(); if (samplePipelineOutputFormat == null) { diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TestMuxer.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TestMuxer.java index b4836e27a2..3b9ea4fe8c 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TestMuxer.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TestMuxer.java @@ -45,7 +45,7 @@ public final class TestMuxer implements Muxer, Dumper.Dumpable { // Muxer implementation. @Override - public int addTrack(Format format) { + public int addTrack(Format format) throws MuxerException { int trackIndex = muxer.addTrack(format); dumpables.add(new DumpableFormat(format, trackIndex)); return trackIndex; @@ -53,13 +53,14 @@ public final class TestMuxer implements Muxer, Dumper.Dumpable { @Override public void writeSampleData( - int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) { + int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) + throws MuxerException { dumpables.add(new DumpableSample(trackIndex, data, isKeyFrame, presentationTimeUs)); muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs); } @Override - public void release(boolean forCancellation) { + public void release(boolean forCancellation) throws MuxerException { dumpables.add(dumper -> dumper.add("released", true)); muxer.release(forCancellation); } From a93e8cc620e12dfcba6b5ee40cf718d3fba2868a Mon Sep 17 00:00:00 2001 From: hschlueter Date: Tue, 11 Jan 2022 16:35:08 +0000 Subject: [PATCH 12/28] Update Muxer exception javadoc to match MuxerWrapper. PiperOrigin-RevId: 421039869 --- .../java/com/google/android/exoplayer2/transformer/Muxer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Muxer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Muxer.java index fc6ae6d706..aa83f2a497 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Muxer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Muxer.java @@ -109,7 +109,7 @@ import java.nio.ByteBuffer; * @param data Buffer containing the sample data to write to the container. * @param isKeyFrame Whether the sample is a key frame. * @param presentationTimeUs The presentation time of the sample in microseconds. - * @throws MuxerException If the muxer fails to start or an error occurs while writing the sample. + * @throws MuxerException If the muxer fails to write the sample. */ void writeSampleData(int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) throws MuxerException; From bf32ae50d72b8d05dbf7436e83007314353757bb Mon Sep 17 00:00:00 2001 From: hschlueter Date: Tue, 11 Jan 2022 16:42:12 +0000 Subject: [PATCH 13/28] Remove MediaCodecAdapter dependency from Transformer. Codec and its factories can use MediaCodec directly as for API >= 21, the SynchronousMediaCodecAdapter methods used in Codec just correspond to a single MediaCodec call each so there is no reason to have another wrapping layer. PiperOrigin-RevId: 421041177 --- .../android/exoplayer2/transformer/Codec.java | 47 +++--- .../transformer/DefaultCodecFactory.java | 149 +++++++++--------- 2 files changed, 100 insertions(+), 96 deletions(-) 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 index 7322708008..b95bc862a2 100644 --- 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 @@ -27,7 +27,6 @@ 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; @@ -35,11 +34,11 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** - * A wrapper around {@link MediaCodecAdapter}. + * A wrapper around {@link MediaCodec}. * - *

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. + *

Provides a layer of abstraction for callers that need to interact with {@link MediaCodec}. + * 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 { @@ -64,11 +63,12 @@ public final class Codec { * * @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. + * @param outputSurface 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; + Codec createForVideoDecoding(Format format, Surface outputSurface) + throws TransformationException; } /** A factory for {@link Codec encoder} instances. */ @@ -106,7 +106,8 @@ public final class Codec { private static final int MEDIA_CODEC_PCM_ENCODING = C.ENCODING_PCM_16BIT; private final BufferInfo outputBufferInfo; - private final MediaCodecAdapter mediaCodecAdapter; + private final MediaCodec mediaCodec; + @Nullable private final Surface inputSurface; private @MonotonicNonNull Format outputFormat; @Nullable private ByteBuffer outputBuffer; @@ -116,9 +117,10 @@ public final class Codec { private boolean inputStreamEnded; private boolean outputStreamEnded; - /** Creates a {@code Codec} from a configured and started {@link MediaCodecAdapter}. */ - public Codec(MediaCodecAdapter mediaCodecAdapter) { - this.mediaCodecAdapter = mediaCodecAdapter; + /** Creates a {@code Codec} from a configured and started {@link MediaCodec}. */ + public Codec(MediaCodec mediaCodec, @Nullable Surface inputSurface) { + this.mediaCodec = mediaCodec; + this.inputSurface = inputSurface; outputBufferInfo = new BufferInfo(); inputBufferIndex = C.INDEX_UNSET; outputBufferIndex = C.INDEX_UNSET; @@ -127,7 +129,7 @@ public final class Codec { /** Returns the input {@link Surface}, or null if the input is not a surface. */ @Nullable public Surface getInputSurface() { - return mediaCodecAdapter.getInputSurface(); + return inputSurface; } /** @@ -142,11 +144,11 @@ public final class Codec { return false; } if (inputBufferIndex < 0) { - inputBufferIndex = mediaCodecAdapter.dequeueInputBufferIndex(); + inputBufferIndex = mediaCodec.dequeueInputBuffer(/* timeoutUs= */ 0); if (inputBufferIndex < 0) { return false; } - inputBuffer.data = mediaCodecAdapter.getInputBuffer(inputBufferIndex); + inputBuffer.data = mediaCodec.getInputBuffer(inputBufferIndex); inputBuffer.clear(); } checkNotNull(inputBuffer.data); @@ -172,13 +174,13 @@ public final class Codec { inputStreamEnded = true; flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM; } - mediaCodecAdapter.queueInputBuffer(inputBufferIndex, offset, size, inputBuffer.timeUs, flags); + mediaCodec.queueInputBuffer(inputBufferIndex, offset, size, inputBuffer.timeUs, flags); inputBufferIndex = C.INDEX_UNSET; inputBuffer.data = null; } public void signalEndOfInputStream() { - mediaCodecAdapter.signalEndOfInputStream(); + mediaCodec.signalEndOfInputStream(); } /** Returns the current output format, if available. */ @@ -222,7 +224,7 @@ public final class Codec { */ public void releaseOutputBuffer(boolean render) { outputBuffer = null; - mediaCodecAdapter.releaseOutputBuffer(outputBufferIndex, render); + mediaCodec.releaseOutputBuffer(outputBufferIndex, render); outputBufferIndex = C.INDEX_UNSET; } @@ -234,7 +236,10 @@ public final class Codec { /** Releases the underlying codec. */ public void release() { outputBuffer = null; - mediaCodecAdapter.release(); + if (inputSurface != null) { + inputSurface.release(); + } + mediaCodec.release(); } /** @@ -247,7 +252,7 @@ public final class Codec { return false; } - outputBuffer = checkNotNull(mediaCodecAdapter.getOutputBuffer(outputBufferIndex)); + outputBuffer = checkNotNull(mediaCodec.getOutputBuffer(outputBufferIndex)); outputBuffer.position(outputBufferInfo.offset); outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); return true; @@ -265,10 +270,10 @@ public final class Codec { return false; } - outputBufferIndex = mediaCodecAdapter.dequeueOutputBufferIndex(outputBufferInfo); + outputBufferIndex = mediaCodec.dequeueOutputBuffer(outputBufferInfo, /* timeoutUs= */ 0); if (outputBufferIndex < 0) { if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - outputFormat = getFormat(mediaCodecAdapter.getOutputFormat()); + outputFormat = getFormat(mediaCodec.getOutputFormat()); } return false; } 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 index 22603002c4..80c43bd818 100644 --- 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 @@ -25,29 +25,17 @@ import android.media.MediaCodec; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaFormat; import android.view.Surface; +import androidx.annotation.Nullable; 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 com.google.android.exoplayer2.util.TraceUtil; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** A default {@link Codec.DecoderFactory} and {@link Codec.EncoderFactory}. */ /* package */ final class DefaultCodecFactory implements Codec.DecoderFactory, Codec.EncoderFactory { - private static final MediaCodecInfo PLACEHOLDER_MEDIA_CODEC_INFO = - 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); - @Override public Codec createForAudioDecoding(Format format) throws TransformationException { MediaFormat mediaFormat = @@ -57,22 +45,17 @@ import java.io.IOException; mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); - MediaCodecAdapter adapter; - try { - adapter = - new MediaCodecFactory() - .createAdapter( - MediaCodecAdapter.Configuration.createForAudioDecoding( - PLACEHOLDER_MEDIA_CODEC_INFO, mediaFormat, format, /* crypto= */ null)); - } catch (Exception e) { - throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ true); - } - return new Codec(adapter); + return createCodec( + format, + mediaFormat, + /* isVideo= */ false, + /* isDecoder= */ true, + /* outputSurface= */ null); } @Override @SuppressLint("InlinedApi") - public Codec createForVideoDecoding(Format format, Surface surface) + public Codec createForVideoDecoding(Format format, Surface outputSurface) throws TransformationException { MediaFormat mediaFormat = MediaFormat.createVideoFormat( @@ -87,21 +70,8 @@ import java.io.IOException; mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0); } - MediaCodecAdapter adapter; - try { - adapter = - new MediaCodecFactory() - .createAdapter( - MediaCodecAdapter.Configuration.createForVideoDecoding( - PLACEHOLDER_MEDIA_CODEC_INFO, - mediaFormat, - format, - surface, - /* crypto= */ null)); - } catch (Exception e) { - throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ true); - } - return new Codec(adapter); + return createCodec( + format, mediaFormat, /* isVideo= */ true, /* isDecoder= */ true, outputSurface); } @Override @@ -111,17 +81,12 @@ import java.io.IOException; 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( - PLACEHOLDER_MEDIA_CODEC_INFO, mediaFormat, format)); - } catch (Exception e) { - throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ false); - } - return new Codec(adapter); + return createCodec( + format, + mediaFormat, + /* isVideo= */ false, + /* isDecoder= */ false, + /* outputSurface= */ null); } @Override @@ -141,36 +106,70 @@ import java.io.IOException; 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( - PLACEHOLDER_MEDIA_CODEC_INFO, mediaFormat, format)); - } catch (Exception e) { - throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ false); - } - return new Codec(adapter); + return createCodec( + format, + mediaFormat, + /* isVideo= */ true, + /* isDecoder= */ false, + /* outputSurface= */ null); } - 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)); + @RequiresNonNull("#1.sampleMimeType") + private static Codec createCodec( + Format format, + MediaFormat mediaFormat, + boolean isVideo, + boolean isDecoder, + @Nullable Surface outputSurface) + throws TransformationException { + @Nullable MediaCodec mediaCodec = null; + @Nullable Surface inputSurface = null; + try { + mediaCodec = + isDecoder + ? MediaCodec.createDecoderByType(format.sampleMimeType) + : MediaCodec.createEncoderByType(format.sampleMimeType); + configureCodec(mediaCodec, mediaFormat, isDecoder, outputSurface); + if (isVideo && !isDecoder) { + inputSurface = mediaCodec.createInputSurface(); + } + startCodec(mediaCodec); + } catch (Exception e) { + if (inputSurface != null) { + inputSurface.release(); + } + if (mediaCodec != null) { + mediaCodec.release(); + } + throw createTransformationException(e, format, isVideo, isDecoder); } + return new Codec(mediaCodec, inputSurface); + } + + private static void configureCodec( + MediaCodec codec, + MediaFormat mediaFormat, + boolean isDecoder, + @Nullable Surface outputSurface) { + TraceUtil.beginSection("configureCodec"); + codec.configure( + mediaFormat, + outputSurface, + /* crypto= */ null, + isDecoder ? 0 : MediaCodec.CONFIGURE_FLAG_ENCODE); + TraceUtil.endSection(); + } + + private static void startCodec(MediaCodec codec) { + TraceUtil.beginSection("startCodec"); + codec.start(); + TraceUtil.endSection(); } private static TransformationException createTransformationException( Exception cause, Format format, boolean isVideo, boolean isDecoder) { String componentName = (isVideo ? "Video" : "Audio") + (isDecoder ? "Decoder" : "Encoder"); - if (cause instanceof IOException) { + if (cause instanceof IOException || cause instanceof MediaCodec.CodecException) { return TransformationException.createForCodec( cause, componentName, From f8d84eec5962f7eb9c7c10899ec44a796579eedd Mon Sep 17 00:00:00 2001 From: hschlueter Date: Tue, 11 Jan 2022 17:13:33 +0000 Subject: [PATCH 14/28] Allow multiple Transformer listeners to be registered. Multiple listeners can be added to Transformer and its builder. All or specific listeners can also be removed. PiperOrigin-RevId: 421047650 --- RELEASENOTES.md | 1 + docs/transforming-media.md | 4 +- .../android/exoplayer2/util/ListenerSet.java | 20 +++ .../transformer/mh/AndroidTestUtil.java | 2 +- .../exoplayer2/transformer/Transformer.java | 121 +++++++++++++++--- .../transformer/TransformerTest.java | 74 +++++++++++ .../transformer/TransformerTestRunner.java | 5 +- 7 files changed, 201 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a55d2fc1d3..e5c67f3a36 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -83,6 +83,7 @@ * `TransformationException` is now used to describe errors that occur during a transformation. * Add `TransformationRequest` for specifying the transformation options. + * Allow multiple listeners to be registered. * 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 a3c112f77a..81f54a8656 100644 --- a/docs/transforming-media.md +++ b/docs/transforming-media.md @@ -34,7 +34,7 @@ transformation that removes the audio track from the input: Transformer transformer = new Transformer.Builder(context) .setRemoveAudio(true) - .setListener(transformerListener) + .addListener(transformerListener) .build(); // Start the transformation. transformer.startTransformation(inputMediaItem, outputPath); @@ -121,7 +121,7 @@ method. Transformer transformer = new Transformer.Builder(context) .setFlattenForSlowMotion(true) - .setListener(transformerListener) + .addListener(transformerListener) .build(); transformer.startTransformation(inputMediaItem, outputPath); ~~~ diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java index 8aa4025bca..e347f534fb 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java @@ -116,6 +116,21 @@ public final class ListenerSet { */ @CheckResult public ListenerSet copy(Looper looper, IterationFinishedEvent iterationFinishedEvent) { + return copy(looper, clock, iterationFinishedEvent); + } + + /** + * Copies the listener set. + * + * @param looper The new {@link Looper} for the copied listener set. + * @param clock The new {@link Clock} for the copied listener set. + * @param iterationFinishedEvent The new {@link IterationFinishedEvent} sent when all other events + * sent during one {@link Looper} message queue iteration were handled by the listeners. + * @return The copied listener set. + */ + @CheckResult + public ListenerSet copy( + Looper looper, Clock clock, IterationFinishedEvent iterationFinishedEvent) { return new ListenerSet<>(listeners, looper, clock, iterationFinishedEvent); } @@ -150,6 +165,11 @@ public final class ListenerSet { } } + /** Removes all listeners from the set. */ + public void clear() { + listeners.clear(); + } + /** Returns the number of added listeners. */ public int size() { return listeners.size(); 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 6f54d93949..503aaaa223 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 @@ -72,7 +72,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Transformer testTransformer = transformer .buildUpon() - .setListener( + .addListener( new Transformer.Listener() { @Override public void onTransformationCompleted(MediaItem inputMediaItem) { 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 6005b0f972..52f0e96f61 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 @@ -54,6 +54,7 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.ListenerSet; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; @@ -98,7 +99,7 @@ public final class Transformer { private boolean removeVideo; private String containerMimeType; private TransformationRequest transformationRequest; - private Transformer.Listener listener; + private ListenerSet listeners; private DebugViewProvider debugViewProvider; private Looper looper; private Clock clock; @@ -108,9 +109,9 @@ public final class Transformer { @Deprecated public Builder() { muxerFactory = new FrameworkMuxer.Factory(); - listener = new Listener() {}; looper = Util.getCurrentOrMainLooper(); clock = Clock.DEFAULT; + listeners = new ListenerSet<>(looper, clock, (listener, flags) -> {}); encoderFactory = Codec.EncoderFactory.DEFAULT; debugViewProvider = DebugViewProvider.NONE; containerMimeType = MimeTypes.VIDEO_MP4; @@ -125,9 +126,9 @@ public final class Transformer { public Builder(Context context) { this.context = context.getApplicationContext(); muxerFactory = new FrameworkMuxer.Factory(); - listener = new Listener() {}; looper = Util.getCurrentOrMainLooper(); clock = Clock.DEFAULT; + listeners = new ListenerSet<>(looper, clock, (listener, flags) -> {}); encoderFactory = Codec.EncoderFactory.DEFAULT; debugViewProvider = DebugViewProvider.NONE; containerMimeType = MimeTypes.VIDEO_MP4; @@ -143,7 +144,7 @@ public final class Transformer { this.removeVideo = transformer.removeVideo; this.containerMimeType = transformer.containerMimeType; this.transformationRequest = transformer.transformationRequest; - this.listener = transformer.listener; + this.listeners = transformer.listeners; this.looper = transformer.looper; this.encoderFactory = transformer.encoderFactory; this.debugViewProvider = transformer.debugViewProvider; @@ -265,15 +266,51 @@ public final class Transformer { } /** - * Sets the {@link Transformer.Listener} to listen to the transformation events. + * @deprecated Use {@link #addListener(Listener)}, {@link #removeListener(Listener)} or {@link + * #removeAllListeners()} instead. + */ + @Deprecated + public Builder setListener(Transformer.Listener listener) { + this.listeners.clear(); + this.listeners.add(listener); + return this; + } + + /** + * Adds a {@link Transformer.Listener} to listen to the transformation events. * - *

This is equivalent to {@link Transformer#setListener(Listener)}. + *

This is equivalent to {@link Transformer#addListener(Listener)}. * * @param listener A {@link Transformer.Listener}. * @return This builder. */ - public Builder setListener(Transformer.Listener listener) { - this.listener = listener; + public Builder addListener(Transformer.Listener listener) { + this.listeners.add(listener); + return this; + } + + /** + * Removes a {@link Transformer.Listener}. + * + *

This is equivalent to {@link Transformer#removeListener(Listener)}. + * + * @param listener A {@link Transformer.Listener}. + * @return This builder. + */ + public Builder removeListener(Transformer.Listener listener) { + this.listeners.remove(listener); + return this; + } + + /** + * Removes all {@link Transformer.Listener listeners}. + * + *

This is equivalent to {@link Transformer#removeAllListeners()}. + * + * @return This builder. + */ + public Builder removeAllListeners() { + this.listeners.clear(); return this; } @@ -288,6 +325,7 @@ public final class Transformer { */ public Builder setLooper(Looper looper) { this.looper = looper; + this.listeners = listeners.copy(looper, (listener, flags) -> {}); return this; } @@ -328,6 +366,7 @@ public final class Transformer { @VisibleForTesting /* package */ Builder setClock(Clock clock) { this.clock = clock; + this.listeners = listeners.copy(looper, clock, (listener, flags) -> {}); return this; } @@ -381,7 +420,7 @@ public final class Transformer { removeVideo, containerMimeType, transformationRequest, - listener, + listeners, looper, clock, encoderFactory, @@ -480,8 +519,8 @@ public final class Transformer { private final Codec.EncoderFactory encoderFactory; private final Codec.DecoderFactory decoderFactory; private final Transformer.DebugViewProvider debugViewProvider; + private final ListenerSet listeners; - private Transformer.Listener listener; @Nullable private MuxerWrapper muxerWrapper; @Nullable private ExoPlayer player; @ProgressState private int progressState; @@ -494,7 +533,7 @@ public final class Transformer { boolean removeVideo, String containerMimeType, TransformationRequest transformationRequest, - Transformer.Listener listener, + ListenerSet listeners, Looper looper, Clock clock, Codec.EncoderFactory encoderFactory, @@ -508,7 +547,7 @@ public final class Transformer { this.removeVideo = removeVideo; this.containerMimeType = containerMimeType; this.transformationRequest = transformationRequest; - this.listener = listener; + this.listeners = listeners; this.looper = looper; this.clock = clock; this.encoderFactory = encoderFactory; @@ -523,20 +562,52 @@ public final class Transformer { } /** - * Sets the {@link Transformer.Listener} to listen to the transformation events. + * @deprecated Use {@link #addListener(Listener)}, {@link #removeListener(Listener)} or {@link + * #removeAllListeners()} instead. + */ + @Deprecated + public void setListener(Transformer.Listener listener) { + verifyApplicationThread(); + this.listeners.clear(); + this.listeners.add(listener); + } + + /** + * Adds a {@link Transformer.Listener} to listen to the transformation events. * * @param listener A {@link Transformer.Listener}. * @throws IllegalStateException If this method is called from the wrong thread. */ - public void setListener(Transformer.Listener listener) { + public void addListener(Transformer.Listener listener) { verifyApplicationThread(); - this.listener = listener; + this.listeners.add(listener); + } + + /** + * Removes a {@link Transformer.Listener}. + * + * @param listener A {@link Transformer.Listener}. + * @throws IllegalStateException If this method is called from the wrong thread. + */ + public void removeListener(Transformer.Listener listener) { + verifyApplicationThread(); + this.listeners.remove(listener); + } + + /** + * Removes all {@link Transformer.Listener listeners}. + * + * @throws IllegalStateException If this method is called from the wrong thread. + */ + public void removeAllListeners() { + verifyApplicationThread(); + this.listeners.clear(); } /** * Starts an asynchronous operation to transform the given {@link MediaItem}. * - *

The transformation state is notified through the {@link Builder#setListener(Listener) + *

The transformation state is notified through the {@link Builder#addListener(Listener) * listener}. * *

Concurrent transformations on the same Transformer object are not allowed. @@ -559,7 +630,7 @@ public final class Transformer { /** * Starts an asynchronous operation to transform the given {@link MediaItem}. * - *

The transformation state is notified through the {@link Builder#setListener(Listener) + *

The transformation state is notified through the {@link Builder#addListener(Listener) * listener}. * *

Concurrent transformations on the same Transformer object are not allowed. @@ -840,16 +911,26 @@ public final class Transformer { } if (exception == null && resourceReleaseException == null) { - listener.onTransformationCompleted(mediaItem); + // TODO(b/213341814): Add event flags for Transformer events. + listeners.queueEvent( + /* eventFlag= */ C.INDEX_UNSET, + listener -> listener.onTransformationCompleted(mediaItem)); + listeners.flushEvents(); return; } if (exception != null) { - listener.onTransformationError(mediaItem, exception); + listeners.queueEvent( + /* eventFlag= */ C.INDEX_UNSET, + listener -> listener.onTransformationError(mediaItem, exception)); } if (resourceReleaseException != null) { - listener.onTransformationError(mediaItem, resourceReleaseException); + TransformationException finalResourceReleaseException = resourceReleaseException; + listeners.queueEvent( + /* eventFlag= */ C.INDEX_UNSET, + listener -> listener.onTransformationError(mediaItem, finalResourceReleaseException)); } + listeners.flushEvents(); } } } 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 910bac7c57..376a0e08b4 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 @@ -22,6 +22,9 @@ import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STA import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_WAITING_FOR_AVAILABILITY; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import android.content.Context; import android.media.MediaCrypto; @@ -248,6 +251,77 @@ public final class TransformerTest { context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO + ".novideo")); } + @Test + public void startTransformation_withMultipleListeners_callsEachOnCompletion() throws Exception { + Transformer.Listener mockListener1 = mock(Transformer.Listener.class); + Transformer.Listener mockListener2 = mock(Transformer.Listener.class); + Transformer.Listener mockListener3 = mock(Transformer.Listener.class); + Transformer transformer = + new Transformer.Builder(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .addListener(mockListener1) + .addListener(mockListener2) + .addListener(mockListener3) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_VIDEO); + + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + + verify(mockListener1, times(1)).onTransformationCompleted(mediaItem); + verify(mockListener2, times(1)).onTransformationCompleted(mediaItem); + verify(mockListener3, times(1)).onTransformationCompleted(mediaItem); + } + + @Test + public void startTransformation_withMultipleListeners_callsEachOnError() throws Exception { + Transformer.Listener mockListener1 = mock(Transformer.Listener.class); + Transformer.Listener mockListener2 = mock(Transformer.Listener.class); + Transformer.Listener mockListener3 = mock(Transformer.Listener.class); + Transformer transformer = + new Transformer.Builder(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .addListener(mockListener1) + .addListener(mockListener2) + .addListener(mockListener3) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_UNSUPPORTED_BY_MUXER); + + transformer.startTransformation(mediaItem, outputPath); + TransformationException exception = TransformerTestRunner.runUntilError(transformer); + + verify(mockListener1, times(1)).onTransformationError(mediaItem, exception); + verify(mockListener2, times(1)).onTransformationError(mediaItem, exception); + verify(mockListener3, times(1)).onTransformationError(mediaItem, exception); + } + + @Test + public void startTransformation_afterBuildUponWithListenerRemoved_onlyCallsRemainingListeners() + throws Exception { + Transformer.Listener mockListener1 = mock(Transformer.Listener.class); + Transformer.Listener mockListener2 = mock(Transformer.Listener.class); + Transformer.Listener mockListener3 = mock(Transformer.Listener.class); + Transformer transformer1 = + new Transformer.Builder(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .addListener(mockListener1) + .addListener(mockListener2) + .addListener(mockListener3) + .build(); + Transformer transformer2 = transformer1.buildUpon().removeListener(mockListener2).build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_VIDEO); + + transformer2.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer2); + + verify(mockListener1, times(1)).onTransformationCompleted(mediaItem); + verify(mockListener2, times(0)).onTransformationCompleted(mediaItem); + verify(mockListener3, times(1)).onTransformationCompleted(mediaItem); + } + @Test public void startTransformation_flattenForSlowMotion_completesSuccessfully() throws Exception { Transformer transformer = 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 ba3063f175..0b88257c73 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 @@ -69,7 +69,7 @@ public final class TransformerTestRunner { private static TransformationException runUntilListenerCalled(Transformer transformer) throws TimeoutException { TransformationResult transformationResult = new TransformationResult(); - Transformer.Listener listener = + transformer.addListener( new Transformer.Listener() { @Override public void onTransformationCompleted(MediaItem inputMediaItem) { @@ -81,8 +81,7 @@ public final class TransformerTestRunner { MediaItem inputMediaItem, TransformationException exception) { transformationResult.exception = exception; } - }; - transformer.setListener(listener); + }); runLooperUntil( transformer.getApplicationLooper(), () -> transformationResult.isCompleted || transformationResult.exception != null); From d18c572d24babdbb5ce53b3017309c28f5d21d61 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Wed, 12 Jan 2022 14:05:49 +0000 Subject: [PATCH 15/28] Add a Builder for TransformationResult. PiperOrigin-RevId: 421278099 --- .../transformer/mh/AndroidTestUtil.java | 55 +++++++++++++++---- .../RepeatedTranscodeTransformationTest.java | 48 ++++++++-------- 2 files changed, 68 insertions(+), 35 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 503aaaa223..184410a5c9 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 @@ -43,12 +43,45 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Information about the result of successfully running a transformer. */ public static final class TransformationResult { - public final String testId; - public final long outputSizeBytes; - private TransformationResult(String testId, long outputSizeBytes) { + /** A builder for {@link TransformationResult} instances. */ + public static final class Builder { + private final String testId; + @Nullable private Long fileSizeBytes; + + public Builder(String testId) { + this.testId = testId; + } + + public Builder setFileSizeBytes(long fileSizeBytes) { + this.fileSizeBytes = fileSizeBytes; + return this; + } + + public TransformationResult build() { + return new TransformationResult(testId, fileSizeBytes); + } + } + + public final String testId; + @Nullable public final Long fileSizeBytes; + + private TransformationResult(String testId, @Nullable Long fileSizeBytes) { this.testId = testId; - this.outputSizeBytes = outputSizeBytes; + this.fileSizeBytes = fileSizeBytes; + } + + /** + * Returns all the analysis data from the test. + * + *

If a value was not generated, it will not be part of the return value. + */ + public String getFormattedAnalysis() { + String analysis = "test=" + testId; + if (fileSizeBytes != null) { + analysis += ", fileSizeBytes=" + fileSizeBytes; + } + return analysis; } } @@ -108,9 +141,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (exception != null) { throw exception; } - long outputSizeBytes = outputVideoFile.length(); - TransformationResult result = new TransformationResult(testId, outputSizeBytes); + TransformationResult result = + new TransformationResult.Builder(testId).setFileSizeBytes(outputVideoFile.length()).build(); + writeTransformationResultToFile(context, result); return result; } @@ -121,16 +155,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; createExternalCacheFile(context, /* fileName= */ result.testId + "-result.txt"); try (FileWriter fileWriter = new FileWriter(analysisFile)) { String fileContents = - "test=" - + result.testId + result.getFormattedAnalysis() + + ", deviceFingerprint=" + + Build.FINGERPRINT + ", deviceBrand=" + Build.MANUFACTURER + ", deviceModel=" + Build.MODEL + ", sdkVersion=" - + Build.VERSION.SDK_INT - + ", outputSizeBytes=" - + result.outputSizeBytes; + + Build.VERSION.SDK_INT; fileWriter.write(fileContents); } } 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 636d289f1e..93f5147a16 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 @@ -24,16 +24,16 @@ 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.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import java.util.HashSet; import java.util.Set; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; /** Tests repeated transcoding operations (as a stress test and to help reproduce flakiness). */ @RunWith(AndroidJUnit4.class) -@Ignore("Internal - b/206917996") +// @Ignore("Internal - b/206917996") public final class RepeatedTranscodeTransformationTest { private static final int TRANSCODE_COUNT = 10; @@ -55,14 +55,14 @@ public final class RepeatedTranscodeTransformationTest { Set differentOutputSizesBytes = new HashSet<>(); for (int i = 0; i < TRANSCODE_COUNT; i++) { // Use a long video in case an error occurs a while after the start of the video. - differentOutputSizesBytes.add( + AndroidTestUtil.TransformationResult result = runTransformer( - context, - /* testId= */ "repeatedTranscode_givesConsistentLengthOutput", - transformer, - AndroidTestUtil.REMOTE_MP4_10_SECONDS_URI_STRING, - /* timeoutSeconds= */ 120) - .outputSizeBytes); + context, + /* testId= */ "repeatedTranscode_givesConsistentLengthOutput_" + i, + transformer, + AndroidTestUtil.REMOTE_MP4_10_SECONDS_URI_STRING, + /* timeoutSeconds= */ 120); + differentOutputSizesBytes.add(Assertions.checkNotNull(result.fileSizeBytes)); } assertWithMessage( @@ -89,14 +89,14 @@ public final class RepeatedTranscodeTransformationTest { Set differentOutputSizesBytes = new HashSet<>(); for (int i = 0; i < TRANSCODE_COUNT; i++) { // Use a long video in case an error occurs a while after the start of the video. - differentOutputSizesBytes.add( + AndroidTestUtil.TransformationResult result = runTransformer( - context, - /* testId= */ "repeatedTranscodeNoAudio_givesConsistentLengthOutput", - transformer, - AndroidTestUtil.REMOTE_MP4_10_SECONDS_URI_STRING, - /* timeoutSeconds= */ 120) - .outputSizeBytes); + context, + /* testId= */ "repeatedTranscodeNoAudio_givesConsistentLengthOutput_" + i, + transformer, + AndroidTestUtil.REMOTE_MP4_10_SECONDS_URI_STRING, + /* timeoutSeconds= */ 120); + differentOutputSizesBytes.add(Assertions.checkNotNull(result.fileSizeBytes)); } assertWithMessage( @@ -108,7 +108,7 @@ public final class RepeatedTranscodeTransformationTest { @Test public void repeatedTranscodeNoVideo_givesConsistentLengthOutput() throws Exception { Context context = ApplicationProvider.getApplicationContext(); - Transformer transcodingTransformer = + Transformer transformer = new Transformer.Builder(context) .setRemoveVideo(true) .setTransformationRequest( @@ -120,14 +120,14 @@ public final class RepeatedTranscodeTransformationTest { Set differentOutputSizesBytes = new HashSet<>(); for (int i = 0; i < TRANSCODE_COUNT; i++) { // Use a long video in case an error occurs a while after the start of the video. - differentOutputSizesBytes.add( + AndroidTestUtil.TransformationResult result = runTransformer( - context, - /* testId= */ "repeatedTranscodeNoVideo_givesConsistentLengthOutput", - transcodingTransformer, - AndroidTestUtil.REMOTE_MP4_10_SECONDS_URI_STRING, - /* timeoutSeconds= */ 120) - .outputSizeBytes); + context, + /* testId= */ "repeatedTranscodeNoVideo_givesConsistentLengthOutput_" + i, + transformer, + AndroidTestUtil.REMOTE_MP4_10_SECONDS_URI_STRING, + /* timeoutSeconds= */ 120); + differentOutputSizesBytes.add(Assertions.checkNotNull(result.fileSizeBytes)); } assertWithMessage( From 4647a747ca51a783bb853c06a576fce2a3c5c564 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Wed, 12 Jan 2022 16:24:42 +0000 Subject: [PATCH 16/28] Uncomment line. Accidentally commented out the Ignore annotation. PiperOrigin-RevId: 421304369 --- .../transformer/mh/RepeatedTranscodeTransformationTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 93f5147a16..a546c4fc70 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 @@ -28,12 +28,13 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import java.util.HashSet; import java.util.Set; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; /** Tests repeated transcoding operations (as a stress test and to help reproduce flakiness). */ @RunWith(AndroidJUnit4.class) -// @Ignore("Internal - b/206917996") +@Ignore("Internal - b/206917996") public final class RepeatedTranscodeTransformationTest { private static final int TRANSCODE_COUNT = 10; From b77204eb4d8a80b4aeae0cfe43f6db636a24851a Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 13 Jan 2022 09:53:57 +0000 Subject: [PATCH 17/28] Update test relying on network type detection to run on all API levels. This ensures we test the API level specific logic, in particular around 5G-NSA detection. Robolectric has a remaining bug that it doesn't support listening to service state changes. Hence, we need to ignore some tests on these API levels still until this is fixed. PiperOrigin-RevId: 421505951 --- .../upstream/DefaultBandwidthMeterTest.java | 147 ++++++++++++++++-- 1 file changed, 137 insertions(+), 10 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java index 532be67521..ec3dedb455 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java @@ -25,6 +25,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.NetworkInfo.DetailedState; import android.net.Uri; +import android.telephony.TelephonyDisplayInfo; import android.telephony.TelephonyManager; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -32,6 +33,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeClock; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.util.NetworkTypeObserver; +import com.google.android.exoplayer2.util.Util; import java.util.Random; import org.junit.Before; import org.junit.Test; @@ -40,9 +42,11 @@ import org.robolectric.Shadows; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLooper; import org.robolectric.shadows.ShadowNetworkInfo; +import org.robolectric.shadows.ShadowTelephonyManager; /** Unit test for {@link DefaultBandwidthMeter}. */ @RunWith(AndroidJUnit4.class) +@Config(sdk = Config.ALL_SDKS) // Test all SDKs because network detection logic changed over time. public final class DefaultBandwidthMeterTest { private static final int SIMULATED_TRANSFER_COUNT = 100; @@ -56,8 +60,6 @@ public final class DefaultBandwidthMeterTest { private NetworkInfo networkInfo2g; private NetworkInfo networkInfo3g; private NetworkInfo networkInfo4g; - // TODO: Add tests covering 5G-NSA networks. Not testable right now because we need to set the - // TelephonyDisplayInfo on API 31, which isn't available for Robolectric yet. private NetworkInfo networkInfo5gSa; private NetworkInfo networkInfoEthernet; @@ -183,9 +185,15 @@ public final class DefaultBandwidthMeterTest { assertThat(initialEstimateEthernet).isGreaterThan(initialEstimate3g); } - @Config(sdk = 28) // TODO(b/190021699): Fix 4G tests to work on newer API levels @Test public void defaultInitialBitrateEstimate_for4G_isGreaterThanEstimateFor2G() { + if (Util.SDK_INT == 29 || Util.SDK_INT == 30) { + // Robolectric doesn't support listening to service state changes, which we need on APIs 29 + // and 30 to run this test successfully. + // TODO(b/190021699): Update once Robolectric released support for this. + return; + } + setActiveNetworkInfo(networkInfo4g); DefaultBandwidthMeter bandwidthMeter4g = new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); @@ -199,9 +207,15 @@ public final class DefaultBandwidthMeterTest { assertThat(initialEstimate4g).isGreaterThan(initialEstimate2g); } - @Config(sdk = 28) // TODO(b/190021699): Fix 4G tests to work on newer API levels @Test public void defaultInitialBitrateEstimate_for4G_isGreaterThanEstimateFor3G() { + if (Util.SDK_INT == 29 || Util.SDK_INT == 30) { + // Robolectric doesn't support listening to service state changes, which we need on APIs 29 + // and 30 to run this test successfully. + // TODO(b/190021699): Update once Robolectric released support for this. + return; + } + setActiveNetworkInfo(networkInfo4g); DefaultBandwidthMeter bandwidthMeter4g = new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); @@ -231,18 +245,42 @@ public final class DefaultBandwidthMeterTest { } @Test - public void defaultInitialBitrateEstimate_for5gSa_isGreaterThanEstimateFor4g() { + @Config(minSdk = 29) // 5G detection support was added in API 29. + public void defaultInitialBitrateEstimate_for5gNsa_isGreaterThanEstimateFor4g() { + if (Util.SDK_INT == 29 || Util.SDK_INT == 30) { + // Robolectric doesn't support listening to service state changes, which we need on APIs 29 + // and 30 to run this test successfully. + // TODO(b/190021699): Update once Robolectric released support for this. + return; + } + setActiveNetworkInfo(networkInfo4g); DefaultBandwidthMeter bandwidthMeter4g = new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); long initialEstimate4g = bandwidthMeter4g.getBitrateEstimate(); + setActiveNetworkInfo(networkInfo4g, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA); + DefaultBandwidthMeter bandwidthMeter5gNsa = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); + long initialEstimate5gNsa = bandwidthMeter5gNsa.getBitrateEstimate(); + + assertThat(initialEstimate5gNsa).isGreaterThan(initialEstimate4g); + } + + @Test + @Config(minSdk = 29) // 5G detection support was added in API 29. + public void defaultInitialBitrateEstimate_for5gSa_isGreaterThanEstimateFor3g() { + setActiveNetworkInfo(networkInfo3g); + DefaultBandwidthMeter bandwidthMeter3g = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); + long initialEstimate3g = bandwidthMeter3g.getBitrateEstimate(); + setActiveNetworkInfo(networkInfo5gSa); DefaultBandwidthMeter bandwidthMeter5gSa = new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); long initialEstimate5gSa = bandwidthMeter5gSa.getBitrateEstimate(); - assertThat(initialEstimate5gSa).isGreaterThan(initialEstimate4g); + assertThat(initialEstimate5gSa).isGreaterThan(initialEstimate3g); } @Test @@ -324,10 +362,16 @@ public final class DefaultBandwidthMeterTest { assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow); } - @Config(sdk = 28) // TODO(b/190021699): Fix 4G tests to work on newer API levels @Test public void defaultInitialBitrateEstimate_for4g_forFastCountry_isGreaterThanEstimateForSlowCountry() { + if (Util.SDK_INT == 29 || Util.SDK_INT == 30) { + // Robolectric doesn't support listening to service state changes, which we need on APIs 29 + // and 30 to run this test successfully. + // TODO(b/190021699): Update once Robolectric released support for this. + return; + } + setActiveNetworkInfo(networkInfo4g); setNetworkCountryIso(FAST_COUNTRY_ISO); DefaultBandwidthMeter bandwidthMeterFast = @@ -343,6 +387,32 @@ public final class DefaultBandwidthMeterTest { } @Test + @Config(minSdk = 29) // 5G detection support was added in API 29. + public void + defaultInitialBitrateEstimate_for5gNsa_forFastCountry_isGreaterThanEstimateForSlowCountry() { + if (Util.SDK_INT == 29 || Util.SDK_INT == 30) { + // Robolectric doesn't support listening to service state changes, which we need on APIs 29 + // and 30 to run this test successfully. + // TODO(b/190021699): Update once Robolectric released support for this. + return; + } + + setActiveNetworkInfo(networkInfo4g, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA); + setNetworkCountryIso(FAST_COUNTRY_ISO); + DefaultBandwidthMeter bandwidthMeterFast = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); + long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate(); + + setNetworkCountryIso(SLOW_COUNTRY_ISO); + DefaultBandwidthMeter bandwidthMeterSlow = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); + long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate(); + + assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow); + } + + @Test + @Config(minSdk = 29) // 5G detection support was added in API 29. public void defaultInitialBitrateEstimate_for5gSa_forFastCountry_isGreaterThanEstimateForSlowCountry() { setActiveNetworkInfo(networkInfo5gSa); @@ -484,9 +554,15 @@ public final class DefaultBandwidthMeterTest { assertThat(initialEstimate).isNotEqualTo(123456789); } - @Config(sdk = 28) // TODO(b/190021699): Fix 4G tests to work on newer API levels @Test public void initialBitrateEstimateOverwrite_for4G_whileConnectedTo4G_setsInitialEstimate() { + if (Util.SDK_INT == 29 || Util.SDK_INT == 30) { + // Robolectric doesn't support listening to service state changes, which we need on APIs 29 + // and 30 to run this test successfully. + // TODO(b/190021699): Update once Robolectric released support for this. + return; + } + setActiveNetworkInfo(networkInfo4g); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()) @@ -497,7 +573,6 @@ public final class DefaultBandwidthMeterTest { assertThat(initialEstimate).isEqualTo(123456789); } - @Config(sdk = 28) // TODO(b/190021699): Fix 4G tests to work on newer API levels @Test public void initialBitrateEstimateOverwrite_for4G_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() { @@ -512,6 +587,41 @@ public final class DefaultBandwidthMeterTest { } @Test + @Config(minSdk = 29) // 5G detection support was added in API 29. + public void initialBitrateEstimateOverwrite_for5gNsa_whileConnectedTo5gNsa_setsInitialEstimate() { + if (Util.SDK_INT == 29 || Util.SDK_INT == 30) { + // Robolectric doesn't support listening to service state changes, which we need on APIs 29 + // and 30 to run this test successfully. + // TODO(b/190021699): Update once Robolectric released support for this. + return; + } + + setActiveNetworkInfo(networkInfo4g, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA); + DefaultBandwidthMeter bandwidthMeter = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()) + .setInitialBitrateEstimate(C.NETWORK_TYPE_5G_NSA, 123456789) + .build(); + long initialEstimate = bandwidthMeter.getBitrateEstimate(); + + assertThat(initialEstimate).isEqualTo(123456789); + } + + @Test + @Config(minSdk = 29) // 5G detection support was added in API 29. + public void + initialBitrateEstimateOverwrite_for5gNsa_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() { + setActiveNetworkInfo(networkInfo4g); + DefaultBandwidthMeter bandwidthMeter = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()) + .setInitialBitrateEstimate(C.NETWORK_TYPE_5G_NSA, 123456789) + .build(); + long initialEstimate = bandwidthMeter.getBitrateEstimate(); + + assertThat(initialEstimate).isNotEqualTo(123456789); + } + + @Test + @Config(minSdk = 29) // 5G detection support was added in API 29. public void initialBitrateEstimateOverwrite_for5gSa_whileConnectedTo5gSa_setsInitialEstimate() { setActiveNetworkInfo(networkInfo5gSa); DefaultBandwidthMeter bandwidthMeter = @@ -524,6 +634,7 @@ public final class DefaultBandwidthMeterTest { } @Test + @Config(minSdk = 29) // 5G detection support was added in API 29. public void initialBitrateEstimateOverwrite_for5gSa_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() { setActiveNetworkInfo(networkInfoWifi); @@ -636,11 +747,27 @@ public final class DefaultBandwidthMeterTest { assertThat(initialEstimateWithoutBuilder).isLessThan(50_000_000L); } - @SuppressWarnings("StickyBroadcast") private void setActiveNetworkInfo(NetworkInfo networkInfo) { + setActiveNetworkInfo(networkInfo, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE); + } + + @SuppressWarnings("StickyBroadcast") + private void setActiveNetworkInfo(NetworkInfo networkInfo, int networkTypeOverride) { + // Set network info in ConnectivityManager and TelephonyDisplayInfo in TelephonyManager. Shadows.shadowOf(connectivityManager).setActiveNetworkInfo(networkInfo); + if (Util.SDK_INT >= 31) { + Object displayInfo = + ShadowTelephonyManager.createTelephonyDisplayInfo( + networkInfo.getType(), networkTypeOverride); + Shadows.shadowOf(telephonyManager).setTelephonyDisplayInfo(displayInfo); + } + // Create a sticky broadcast for the connectivity action because Roboletric isn't replying with + // the current network state if a receiver for this intent is registered. ApplicationProvider.getApplicationContext() .sendStickyBroadcast(new Intent(ConnectivityManager.CONNECTIVITY_ACTION)); + // Trigger initialization of static network type observer. + NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); + // Wait until all pending messages are handled and the network initialization is done. ShadowLooper.idleMainLooper(); } From b09b8dc2aba5f4b54d76a0b958c62aeb5e5b8075 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 13 Jan 2022 10:38:43 +0000 Subject: [PATCH 18/28] Disable live speed adjustment where it has no benefit Live speed adjustment is used for all live playback at the moment, but has no user visible effect if the media is not played with low latency. To avoid unnecessary adjustment during playback without benefit, this change restricts the live speed adjustment to cases where either the user requested a speed value in the MediaItem or the media specifically defined a low-latency stream. Issue: google/ExoPlayer#9329 PiperOrigin-RevId: 421514283 --- RELEASENOTES.md | 5 +- .../DefaultLivePlaybackSpeedControl.java | 4 + .../DefaultLivePlaybackSpeedControlTest.java | 68 +++++++++------ .../source/dash/DashMediaSource.java | 14 ++- .../source/dash/DashMediaSourceTest.java | 75 ++++++++++++++-- .../exoplayer2/source/hls/HlsMediaSource.java | 21 +++-- .../source/hls/HlsMediaSourceTest.java | 85 ++++++++++++++++++- 7 files changed, 225 insertions(+), 47 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e5c67f3a36..1704847087 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,7 +17,7 @@ * Sleep and retry when creating a `MediaCodec` instance fails. This works 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]. + ((#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)). @@ -28,6 +28,9 @@ ((#8353)[https://github.com/google/ExoPlayer/issues/8353]). * Fix decoder fallback logic for Dolby Atmos (E-AC3-JOC) and Dolby Vision to use a compatible base decoder (E-AC3 or H264/H265) if needed. + * Disable automatic speed adjustment for live streams that neither have + low-latency features nor a user request setting the speed + ((#9329)[https://github.com/google/ExoPlayer/issues/9329]). * 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/DefaultLivePlaybackSpeedControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java index 59753448db..26ef315742 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java @@ -308,6 +308,10 @@ public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedC liveConfiguration.maxPlaybackSpeed != C.RATE_UNSET ? liveConfiguration.maxPlaybackSpeed : fallbackMaxPlaybackSpeed; + if (minPlaybackSpeed == 1f && maxPlaybackSpeed == 1f) { + // Don't bother calculating adjustments if it's not possible to change the speed. + mediaConfigurationTargetLiveOffsetUs = C.TIME_UNSET; + } maybeResetTargetLiveOffsetUs(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java index cc05a87558..016e886f6e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java @@ -48,8 +48,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(42_000); @@ -65,8 +65,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(4321) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(400_000); @@ -82,8 +82,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(3) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(5_000); @@ -100,8 +100,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -121,8 +121,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -142,8 +142,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -163,6 +163,22 @@ public class DefaultLivePlaybackSpeedControlTest { assertThat(targetLiveOffsetUs).isEqualTo(C.TIME_UNSET); } + @Test + public void getTargetLiveOffsetUs_withUnitSpeed_returnsUnset() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().build(); + defaultLivePlaybackSpeedControl.setLiveConfiguration( + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); + + long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); + + assertThat(targetLiveOffsetUs).isEqualTo(C.TIME_UNSET); + } + @Test public void getTargetLiveOffsetUs_afterSetTargetLiveOffsetOverrideWithTimeUnset_returnsMediaLiveOffset() { @@ -174,8 +190,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(C.TIME_UNSET); @@ -195,8 +211,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -218,8 +234,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); List targetOffsetsUs = new ArrayList<>(); @@ -244,8 +260,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); @@ -266,8 +282,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); @@ -289,8 +305,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -321,8 +337,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); 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 92f03e29f5..868fe4afce 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 @@ -800,7 +800,7 @@ public final class DashMediaSource extends BaseMediaSource { nowUnixTimeUs - Util.msToUs(manifest.availabilityStartTimeMs) - windowStartTimeInManifestUs; - updateMediaItemLiveConfiguration(nowInWindowUs, windowDurationUs); + updateLiveConfiguration(nowInWindowUs, windowDurationUs); windowStartUnixTimeMs = manifest.availabilityStartTimeMs + Util.usToMs(windowStartTimeInManifestUs); windowDefaultPositionUs = nowInWindowUs - Util.msToUs(liveConfiguration.targetOffsetMs); @@ -859,7 +859,7 @@ public final class DashMediaSource extends BaseMediaSource { } } - private void updateMediaItemLiveConfiguration(long nowInWindowUs, long windowDurationUs) { + private void updateLiveConfiguration(long nowInWindowUs, long windowDurationUs) { // Default maximum offset: start of window. long maxPossibleLiveOffsetMs = usToMs(nowInWindowUs); long maxLiveOffsetMs = maxPossibleLiveOffsetMs; @@ -934,6 +934,16 @@ public final class DashMediaSource extends BaseMediaSource { } else if (manifest.serviceDescription != null) { maxPlaybackSpeed = manifest.serviceDescription.maxPlaybackSpeed; } + if (minPlaybackSpeed == C.RATE_UNSET + && maxPlaybackSpeed == C.RATE_UNSET + && (manifest.serviceDescription == null + || manifest.serviceDescription.targetOffsetMs == C.TIME_UNSET)) { + // Force unit speed (instead of automatic adjustment with fallback speeds) if there are no + // specific speed limits defined by the media item or the manifest, and the manifest contains + // no low-latency target offset either. + minPlaybackSpeed = 1f; + maxPlaybackSpeed = 1f; + } liveConfiguration = new MediaItem.LiveConfiguration.Builder() .setTargetOffsetMs(targetOffsetMs) 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 bebcb074ca..9674453f81 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 @@ -24,6 +24,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaItem.LiveConfiguration; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Window; @@ -140,7 +141,7 @@ public final class DashMediaSourceTest { } @Test - public void prepare_withoutLiveConfiguration_withoutMediaItemLiveProperties_usesDefaultFallback() + public void prepare_withoutLiveConfiguration_withoutMediaItemLiveConfiguration_usesUnitSpeed() throws InterruptedException { DashMediaSource mediaSource = new DashMediaSource.Factory( @@ -154,18 +155,71 @@ public final class DashMediaSourceTest { .isEqualTo(DashMediaSource.DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS); assertThat(liveConfiguration.minOffsetMs).isEqualTo(0L); assertThat(liveConfiguration.maxOffsetMs).isEqualTo(58_000L); - assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); - assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(1f); + assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(1f); } @Test - public void prepare_withoutLiveConfiguration_withoutMediaItemLiveProperties_usesFallback() + public void prepare_withoutLiveConfiguration_withOnlyMediaItemTargetOffset_usesUnitSpeed() throws InterruptedException { + DashMediaSource mediaSource = + new DashMediaSource.Factory( + () -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION)) + .createMediaSource( + new MediaItem.Builder() + .setUri(Uri.EMPTY) + .setLiveConfiguration( + new LiveConfiguration.Builder().setTargetOffsetMs(10_000L).build()) + .build()); + + MediaItem.LiveConfiguration liveConfiguration = + prepareAndWaitForTimelineRefresh(mediaSource).liveConfiguration; + + assertThat(liveConfiguration.targetOffsetMs).isEqualTo(10_000L); + assertThat(liveConfiguration.minOffsetMs).isEqualTo(0L); + assertThat(liveConfiguration.maxOffsetMs).isEqualTo(58_000L); + assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(1f); + assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(1f); + } + + @Test + public void prepare_withoutLiveConfiguration_withMediaItemSpeedLimits_usesDefaultFallbackValues() + throws InterruptedException { + DashMediaSource mediaSource = + new DashMediaSource.Factory( + () -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION)) + .createMediaSource( + new MediaItem.Builder() + .setUri(Uri.EMPTY) + .setLiveConfiguration( + new LiveConfiguration.Builder().setMinPlaybackSpeed(0.95f).build()) + .build()); + + MediaItem.LiveConfiguration liveConfiguration = + prepareAndWaitForTimelineRefresh(mediaSource).liveConfiguration; + + assertThat(liveConfiguration.targetOffsetMs) + .isEqualTo(DashMediaSource.DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS); + assertThat(liveConfiguration.minOffsetMs).isEqualTo(0L); + assertThat(liveConfiguration.maxOffsetMs).isEqualTo(58_000L); + assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(0.95f); + assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); + } + + @Test + public void + prepare_withoutLiveConfiguration_withoutMediaItemTargetOffset_usesDefinedFallbackTargetOffset() + throws InterruptedException { DashMediaSource mediaSource = new DashMediaSource.Factory( () -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION)) .setFallbackTargetLiveOffsetMs(1234L) - .createMediaSource(MediaItem.fromUri(Uri.EMPTY)); + .createMediaSource( + new MediaItem.Builder() + .setUri(Uri.EMPTY) + .setLiveConfiguration( + new LiveConfiguration.Builder().setMinPlaybackSpeed(0.95f).build()) + .build()); MediaItem.LiveConfiguration liveConfiguration = prepareAndWaitForTimelineRefresh(mediaSource).liveConfiguration; @@ -173,7 +227,7 @@ public final class DashMediaSourceTest { assertThat(liveConfiguration.targetOffsetMs).isEqualTo(1234L); assertThat(liveConfiguration.minOffsetMs).isEqualTo(0L); assertThat(liveConfiguration.maxOffsetMs).isEqualTo(58_000L); - assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(0.95f); assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); } @@ -213,7 +267,12 @@ public final class DashMediaSourceTest { createSampleMpdDataSource( SAMPLE_MPD_LIVE_WITH_SUGGESTED_PRESENTATION_DELAY_2S_MIN_BUFFER_TIME_500MS)) .setFallbackTargetLiveOffsetMs(1234L) - .createMediaSource(MediaItem.fromUri(Uri.EMPTY)); + .createMediaSource( + new MediaItem.Builder() + .setUri(Uri.EMPTY) + .setLiveConfiguration( + new LiveConfiguration.Builder().setMaxPlaybackSpeed(1.05f).build()) + .build()); MediaItem.LiveConfiguration liveConfiguration = prepareAndWaitForTimelineRefresh(mediaSource).liveConfiguration; @@ -222,7 +281,7 @@ public final class DashMediaSourceTest { assertThat(liveConfiguration.minOffsetMs).isEqualTo(500L); assertThat(liveConfiguration.maxOffsetMs).isEqualTo(58_000L); assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); - assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(1.05f); } @Test 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 b6028b7649..e7143ffd27 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 @@ -26,6 +26,7 @@ import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaItem.LiveConfiguration; import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -474,7 +475,7 @@ public final class HlsMediaSource extends BaseMediaSource targetLiveOffsetUs = Util.constrainValue( targetLiveOffsetUs, liveEdgeOffsetUs, playlist.durationUs + liveEdgeOffsetUs); - maybeUpdateLiveConfiguration(targetLiveOffsetUs); + updateLiveConfiguration(playlist, targetLiveOffsetUs); long windowDefaultStartPositionUs = getLiveWindowDefaultStartPositionUs(playlist, liveEdgeOffsetUs); boolean suppressPositionProjection = @@ -564,12 +565,18 @@ public final class HlsMediaSource extends BaseMediaSource return segment.relativeStartTimeUs; } - private void maybeUpdateLiveConfiguration(long targetLiveOffsetUs) { - long targetLiveOffsetMs = Util.usToMs(targetLiveOffsetUs); - if (targetLiveOffsetMs != liveConfiguration.targetOffsetMs) { - liveConfiguration = - liveConfiguration.buildUpon().setTargetOffsetMs(targetLiveOffsetMs).build(); - } + private void updateLiveConfiguration(HlsMediaPlaylist playlist, long targetLiveOffsetUs) { + boolean disableSpeedAdjustment = + mediaItem.liveConfiguration.minPlaybackSpeed == C.RATE_UNSET + && mediaItem.liveConfiguration.maxPlaybackSpeed == C.RATE_UNSET + && playlist.serverControl.holdBackUs == C.TIME_UNSET + && playlist.serverControl.partHoldBackUs == C.TIME_UNSET; + liveConfiguration = + new LiveConfiguration.Builder() + .setTargetOffsetMs(Util.usToMs(targetLiveOffsetUs)) + .setMinPlaybackSpeed(disableSpeedAdjustment ? 1f : liveConfiguration.minPlaybackSpeed) + .setMaxPlaybackSpeed(disableSpeedAdjustment ? 1f : liveConfiguration.maxPlaybackSpeed) + .build(); } /** 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 9555fb73b4..9e4ced67f2 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 @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import android.os.SystemClock; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; @@ -77,6 +78,8 @@ public class HlsMediaSourceTest { // The target live offset is picked from target duration (3 * 4 = 12 seconds) and then expressed // in relation to the live edge (12 + 1 seconds). assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(13000); + assertThat(window.liveConfiguration.minPlaybackSpeed).isEqualTo(1f); + assertThat(window.liveConfiguration.maxPlaybackSpeed).isEqualTo(1f); assertThat(window.defaultPositionUs).isEqualTo(4000000); } @@ -100,7 +103,7 @@ public class HlsMediaSourceTest { + "#EXTINF:4.00000,\n" + "fileSequence3.ts\n" + "#EXT-X-SERVER-CONTROL:HOLD-BACK=12"; - // The playlist finishes 1 second before the the current time, therefore there's a live edge + // The playlist finishes 1 second before the current time, therefore there's a live edge // offset of 1 second. SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00")); HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist); @@ -113,6 +116,8 @@ public class HlsMediaSourceTest { // The target live offset is picked from hold back and then expressed in relation to the live // edge (+1 seconds). assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(13000); + assertThat(window.liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(window.liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); assertThat(window.defaultPositionUs).isEqualTo(4000000); } @@ -139,7 +144,7 @@ public class HlsMediaSourceTest { + "#EXTINF:4.00000,\n" + "fileSequence3.ts\n" + "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3"; - // The playlist finishes 1 second before the the current time. + // The playlist finishes 1 second before the current time. SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00")); HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist); MediaItem mediaItem = MediaItem.fromUri(playlistUri); @@ -151,6 +156,8 @@ public class HlsMediaSourceTest { // The target live offset is picked from hold back and then expressed in relation to the live // edge (+1 seconds). assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(13000); + assertThat(window.liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(window.liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); assertThat(window.defaultPositionUs).isEqualTo(4000000); } @@ -182,6 +189,8 @@ public class HlsMediaSourceTest { // The target live offset is picked from part hold back and then expressed in relation to the // live edge (+1 seconds). assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(4000); + assertThat(window.liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(window.liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); assertThat(window.defaultPositionUs).isEqualTo(0); } @@ -395,7 +404,7 @@ public class HlsMediaSourceTest { + "#EXTINF:4.00000,\n" + "fileSequence0.ts\n" + "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3"; - // The playlist finishes 1 second before the the current time. This should not affect the target + // The playlist finishes 1 second before the current time. This should not affect the target // live offset set in the media item. SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:05.0+00:00")); HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist); @@ -415,6 +424,76 @@ public class HlsMediaSourceTest { assertThat(window.defaultPositionUs).isEqualTo(0); } + @Test + public void loadLivePlaylist_noHoldBackInPlaylistAndNoPlaybackSpeedInMediaItem_usesUnitSpeed() + throws TimeoutException, ParserException { + String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; + // The playlist has no hold back defined. + String playlist = + "#EXTM3U\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n" + + "#EXT-X-TARGETDURATION:4\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXTINF:4.00000,\n" + + "fileSequence0.ts"; + // The playlist finishes 1 second before the current time. This should not affect the target + // live offset set in the media item. + SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:05.0+00:00")); + HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(playlistUri) + .setLiveConfiguration( + new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(1000).build()) + .build(); + HlsMediaSource mediaSource = factory.createMediaSource(mediaItem); + + Timeline timeline = prepareAndWaitForTimeline(mediaSource); + + Timeline.Window window = timeline.getWindow(0, new Timeline.Window()); + assertThat(window.liveConfiguration.minPlaybackSpeed).isEqualTo(1f); + assertThat(window.liveConfiguration.maxPlaybackSpeed).isEqualTo(1f); + assertThat(window.defaultPositionUs).isEqualTo(0); + } + + @Test + public void + loadLivePlaylist_noHoldBackInPlaylistAndPlaybackSpeedInMediaItem_usesMediaItemConfiguration() + throws TimeoutException, ParserException { + String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; + // The playlist has no hold back defined. + String playlist = + "#EXTM3U\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n" + + "#EXT-X-TARGETDURATION:4\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXTINF:4.00000,\n" + + "fileSequence0.ts"; + // The playlist finishes 1 second before the current time. This should not affect the target + // live offset set in the media item. + SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:05.0+00:00")); + HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(playlistUri) + .setLiveConfiguration( + new MediaItem.LiveConfiguration.Builder() + .setTargetOffsetMs(1000) + .setMinPlaybackSpeed(0.94f) + .setMaxPlaybackSpeed(1.02f) + .build()) + .build(); + HlsMediaSource mediaSource = factory.createMediaSource(mediaItem); + + Timeline timeline = prepareAndWaitForTimeline(mediaSource); + + Timeline.Window window = timeline.getWindow(0, new Timeline.Window()); + assertThat(window.liveConfiguration).isEqualTo(mediaItem.liveConfiguration); + assertThat(window.defaultPositionUs).isEqualTo(0); + } + @Test public void loadLivePlaylist_targetLiveOffsetLargerThanLiveWindow_targetLiveOffsetIsWithinLiveWindow() From 755df46a6b7ce312de15eb712c61a2f4636cefe2 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Thu, 13 Jan 2022 10:43:52 +0000 Subject: [PATCH 19/28] Remove Transformer-specific things from MediaCodecAdapter. PiperOrigin-RevId: 421514944 --- .../AsynchronousMediaCodecAdapter.java | 25 +----- .../mediacodec/MediaCodecAdapter.java | 90 +------------------ .../SynchronousMediaCodecAdapter.java | 51 +---------- .../testutil/CapturingRenderersFactory.java | 12 --- 4 files changed, 8 insertions(+), 170 deletions(-) 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 c49bdb4035..449833bc7d 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 @@ -109,8 +109,7 @@ import java.nio.ByteBuffer; configuration.mediaFormat, configuration.surface, configuration.crypto, - configuration.flags, - configuration.createInputSurface); + configuration.flags); return codecAdapter; } catch (Exception e) { if (codecAdapter != null) { @@ -139,7 +138,6 @@ import java.nio.ByteBuffer; private final boolean enableImmediateCodecStartAfterFlush; private boolean codecReleased; @State private int state; - @Nullable private Surface inputSurface; private AsynchronousMediaCodecAdapter( MediaCodec codec, @@ -159,15 +157,11 @@ import java.nio.ByteBuffer; @Nullable MediaFormat mediaFormat, @Nullable Surface surface, @Nullable MediaCrypto crypto, - int flags, - boolean createInputSurface) { + int flags) { asynchronousMediaCodecCallback.initialize(codec); TraceUtil.beginSection("configureCodec"); codec.configure(mediaFormat, surface, crypto, flags); TraceUtil.endSection(); - if (createInputSurface) { - inputSurface = codec.createInputSurface(); - } bufferEnqueuer.start(); TraceUtil.beginSection("startCodec"); codec.start(); @@ -223,12 +217,6 @@ import java.nio.ByteBuffer; return codec.getInputBuffer(index); } - @Override - @Nullable - public Surface getInputSurface() { - return inputSurface; - } - @Override @Nullable public ByteBuffer getOutputBuffer(int index) { @@ -263,9 +251,6 @@ import java.nio.ByteBuffer; } state = STATE_SHUT_DOWN; } finally { - if (inputSurface != null) { - inputSurface.release(); - } if (!codecReleased) { codec.release(); codecReleased = true; @@ -301,12 +286,6 @@ import java.nio.ByteBuffer; codec.setVideoScalingMode(scalingMode); } - @Override - public void signalEndOfInputStream() { - maybeBlockOnQueueing(); - codec.signalEndOfInputStream(); - } - @Override @RequiresApi(26) public PersistableBundle getMetrics() { 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 3636cf26ed..69acb3b844 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 @@ -55,13 +55,7 @@ public interface MediaCodecAdapter { Format format, @Nullable MediaCrypto crypto) { return new Configuration( - codecInfo, - mediaFormat, - format, - /* surface= */ null, - crypto, - /* flags= */ 0, - /* createInputSurface= */ false); + codecInfo, mediaFormat, format, /* surface= */ null, crypto, /* flags= */ 0); } /** @@ -80,55 +74,7 @@ public interface MediaCodecAdapter { Format format, @Nullable Surface surface, @Nullable MediaCrypto crypto) { - return new Configuration( - codecInfo, - mediaFormat, - format, - surface, - crypto, - /* flags= */ 0, - /* createInputSurface= */ false); - } - - /** - * Creates a configuration for audio encoding. - * - * @param codecInfo See {@link #codecInfo}. - * @param mediaFormat See {@link #mediaFormat}. - * @param format See {@link #format}. - * @return The created instance. - */ - public static Configuration createForAudioEncoding( - MediaCodecInfo codecInfo, MediaFormat mediaFormat, Format format) { - return new Configuration( - codecInfo, - mediaFormat, - format, - /* surface= */ null, - /* crypto= */ null, - MediaCodec.CONFIGURE_FLAG_ENCODE, - /* createInputSurface= */ false); - } - - /** - * Creates a configuration for video encoding. - * - * @param codecInfo See {@link #codecInfo}. - * @param mediaFormat See {@link #mediaFormat}. - * @param format See {@link #format}. - * @return The created instance. - */ - @RequiresApi(18) - public static Configuration createForVideoEncoding( - MediaCodecInfo codecInfo, MediaFormat mediaFormat, Format format) { - return new Configuration( - codecInfo, - mediaFormat, - format, - /* surface= */ null, - /* crypto= */ null, - MediaCodec.CONFIGURE_FLAG_ENCODE, - /* createInputSurface= */ true); + return new Configuration(codecInfo, mediaFormat, format, surface, crypto, /* flags= */ 0); } /** Information about the {@link MediaCodec} being configured. */ @@ -145,17 +91,8 @@ public interface MediaCodecAdapter { @Nullable public final Surface surface; /** For DRM protected playbacks, a {@link MediaCrypto} to use for decryption. */ @Nullable public final MediaCrypto crypto; - /** - * Specify CONFIGURE_FLAG_ENCODE to configure the component as an encoder. - * - * @see MediaCodec#configure - */ + /** See {@link MediaCodec#configure}. */ public final int flags; - /** - * Whether to request a {@link Surface} and use it as to the input to an encoder. This can only - * be set to {@code true} on API 18+. - */ - public final boolean createInputSurface; private Configuration( MediaCodecInfo codecInfo, @@ -163,15 +100,13 @@ public interface MediaCodecAdapter { Format format, @Nullable Surface surface, @Nullable MediaCrypto crypto, - int flags, - boolean createInputSurface) { + int flags) { this.codecInfo = codecInfo; this.mediaFormat = mediaFormat; this.format = format; this.surface = surface; this.crypto = crypto; this.flags = flags; - this.createInputSurface = createInputSurface; } } @@ -229,14 +164,6 @@ public interface MediaCodecAdapter { @Nullable ByteBuffer getInputBuffer(int index); - /** - * Returns the input {@link Surface}, or null if the input is not a surface. - * - * @see MediaCodec#createInputSurface() - */ - @Nullable - Surface getInputSurface(); - /** * Returns a read-only ByteBuffer for a dequeued output buffer index. * @@ -329,15 +256,6 @@ public interface MediaCodecAdapter { /** Whether the adapter needs to be reconfigured before it is used. */ boolean needsReconfiguration(); - /** - * Signals the encoder of end-of-stream on input. The call can only be used when the encoder - * receives its input from a {@link Surface surface}. - * - * @see MediaCodec#signalEndOfInputStream() - */ - @RequiresApi(18) - void signalEndOfInputStream(); - /** * Returns metrics data about the current codec instance. * 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 cfe3c4e875..ff6ce38bda 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 @@ -25,7 +25,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.PersistableBundle; import android.view.Surface; -import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; @@ -46,7 +45,6 @@ public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { @Override public MediaCodecAdapter createAdapter(Configuration configuration) throws IOException { @Nullable MediaCodec codec = null; - @Nullable Surface inputSurface = null; try { codec = createCodec(configuration); TraceUtil.beginSection("configureCodec"); @@ -56,24 +54,11 @@ public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { configuration.crypto, configuration.flags); TraceUtil.endSection(); - - if (configuration.createInputSurface) { - if (Util.SDK_INT >= 18) { - inputSurface = Api18.createCodecInputSurface(codec); - } else { - throw new IllegalStateException( - "Encoding from a surface is only supported on API 18 and up."); - } - } - TraceUtil.beginSection("startCodec"); codec.start(); TraceUtil.endSection(); - return new SynchronousMediaCodecAdapter(codec, inputSurface); + return new SynchronousMediaCodecAdapter(codec); } catch (IOException | RuntimeException e) { - if (inputSurface != null) { - inputSurface.release(); - } if (codec != null) { codec.release(); } @@ -93,13 +78,11 @@ public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { } private final MediaCodec codec; - @Nullable private final Surface inputSurface; @Nullable private ByteBuffer[] inputByteBuffers; @Nullable private ByteBuffer[] outputByteBuffers; - private SynchronousMediaCodecAdapter(MediaCodec mediaCodec, @Nullable Surface inputSurface) { + private SynchronousMediaCodecAdapter(MediaCodec mediaCodec) { this.codec = mediaCodec; - this.inputSurface = inputSurface; if (Util.SDK_INT < 21) { inputByteBuffers = codec.getInputBuffers(); outputByteBuffers = codec.getOutputBuffers(); @@ -144,12 +127,6 @@ public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { } } - @Override - @Nullable - public Surface getInputSurface() { - return inputSurface; - } - @Override @Nullable public ByteBuffer getOutputBuffer(int index) { @@ -193,18 +170,9 @@ public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { public void release() { inputByteBuffers = null; outputByteBuffers = null; - if (inputSurface != null) { - inputSurface.release(); - } codec.release(); } - @Override - @RequiresApi(18) - public void signalEndOfInputStream() { - Api18.signalEndOfInputStream(codec); - } - @Override @RequiresApi(23) public void setOnFrameRenderedListener(OnFrameRenderedListener listener, Handler handler) { @@ -237,19 +205,4 @@ public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { 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(); - } - - @DoNotInline - public static void signalEndOfInputStream(MediaCodec codec) { - codec.signalEndOfInputStream(); - } - } } 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 1823dac443..fd9ee80234 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 @@ -195,12 +195,6 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa return inputBuffer; } - @Nullable - @Override - public Surface getInputSurface() { - return delegate.getInputSurface(); - } - @Nullable @Override public ByteBuffer getOutputBuffer(int index) { @@ -270,12 +264,6 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa delegate.setVideoScalingMode(scalingMode); } - @RequiresApi(18) - @Override - public void signalEndOfInputStream() { - delegate.signalEndOfInputStream(); - } - @RequiresApi(26) @Override public PersistableBundle getMetrics() { From 8d81bd58d8ecedb1cd640708fa5994624ecb88e9 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 13 Jan 2022 12:26:13 +0000 Subject: [PATCH 20/28] Remove Allocator.release(Allocation[]) and references PiperOrigin-RevId: 421530365 --- .../android/exoplayer2/upstream/Allocator.java | 7 ------- .../exoplayer2/upstream/DefaultAllocator.java | 14 ++------------ 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocator.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocator.java index 22d132dfed..aae5dc9715 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocator.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocator.java @@ -48,13 +48,6 @@ public interface Allocator { */ void release(Allocation allocation); - /** - * Releases an array of {@link Allocation Allocations} back to the allocator. - * - * @param allocations The array of {@link Allocation}s being released. - */ - void release(Allocation[] allocations); - /** * Releases all {@link Allocation Allocations} in the chain starting at the given {@link * AllocationNode}. 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 7797883d20..7caaf4d3f8 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 @@ -31,7 +31,6 @@ public final class DefaultAllocator implements Allocator { private final boolean trimOnReset; private final int individualAllocationSize; @Nullable private final byte[] initialAllocationBlock; - private final Allocation[] singleAllocationReleaseHolder; private int targetBufferSize; private int allocatedCount; @@ -76,7 +75,6 @@ public final class DefaultAllocator implements Allocator { } else { initialAllocationBlock = null; } - singleAllocationReleaseHolder = new Allocation[1]; } public synchronized void reset() { @@ -114,16 +112,8 @@ public final class DefaultAllocator implements Allocator { @Override public synchronized void release(Allocation allocation) { - singleAllocationReleaseHolder[0] = allocation; - release(singleAllocationReleaseHolder); - } - - @Override - public synchronized void release(Allocation[] allocations) { - for (Allocation allocation : allocations) { - availableAllocations[availableCount++] = allocation; - } - allocatedCount -= allocations.length; + availableAllocations[availableCount++] = allocation; + allocatedCount--; // Wake up threads waiting for the allocated size to drop. notifyAll(); } From 05924eaa527f8c604750c287f02978d8cc3f5fed Mon Sep 17 00:00:00 2001 From: huangdarwin Date: Thu, 13 Jan 2022 13:59:02 +0000 Subject: [PATCH 21/28] Transformer GL: Add pixel test instructions for physical devices Expected images are taken on emulators, so a larger acceptable difference from expected images must be accepted on physical devices. PiperOrigin-RevId: 421543441 --- .../transformer/FrameEditorDataProcessingTest.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 index a2762ed080..704022ace8 100644 --- 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 @@ -42,7 +42,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -/** Test for frame processing via {@link FrameEditor#processData()}. */ +/** + * Pixel test for frame processing via {@link FrameEditor#processData()}. Expected images are taken + * from emulators, so tests on physical devices may fail. To test on physical devices, please modify + * the MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE. + */ @RunWith(AndroidJUnit4.class) public final class FrameEditorDataProcessingTest { @@ -54,6 +58,12 @@ public final class FrameEditorDataProcessingTest { * 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. + * + *

To run this test on physical devices, please use a value of 5f, rather than 0.1f. This + * higher value will ignore some very small errors, but will allow for some differences caused by + * graphics implementations to be ignored. When the difference is close to the threshold, manually + * inspect expected/actual bitmaps to confirm failure, as it's possible this is caused by a + * difference in the codec or graphics implementation as opposed to a FrameEditor issue. */ private static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE = 0.1f; /** Timeout for dequeueing buffers from the codec, in microseconds. */ From 80851807f2f97ed7c672374a05062862b8baf5fb Mon Sep 17 00:00:00 2001 From: hschlueter Date: Thu, 13 Jan 2022 15:32:21 +0000 Subject: [PATCH 22/28] Use specific error code for exceptions during encoding/decoding. After this change exceptions throw by MediaCodec during encoding/decoding will result in TransformationExceptions with ERROR_CODE_ENCODING_FAILED/ERROR_CODE_DECODING_FAILED. Before this change ERROR_CODE_FAILED_RUNTIME_CHECK was used. PiperOrigin-RevId: 421560396 --- .../transformer/AudioSamplePipeline.java | 22 +-- .../android/exoplayer2/transformer/Codec.java | 147 +++++++++++++++--- .../transformer/DefaultCodecFactory.java | 14 +- .../transformer/SamplePipeline.java | 10 +- .../transformer/TransformationException.java | 18 ++- .../transformer/TransformerBaseRenderer.java | 10 +- .../transformer/TransformerVideoRenderer.java | 6 +- .../transformer/VideoSamplePipeline.java | 10 +- 8 files changed, 184 insertions(+), 53 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 fb380296b9..505677d046 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 @@ -113,17 +113,17 @@ import java.nio.ByteBuffer; @Override @Nullable - public DecoderInputBuffer dequeueInputBuffer() { + public DecoderInputBuffer dequeueInputBuffer() throws TransformationException { return decoder.maybeDequeueInputBuffer(decoderInputBuffer) ? decoderInputBuffer : null; } @Override - public void queueInputBuffer() { + public void queueInputBuffer() throws TransformationException { decoder.queueInputBuffer(decoderInputBuffer); } @Override - public boolean processData() { + public boolean processData() throws TransformationException { if (sonicAudioProcessor.isActive()) { return feedEncoderFromSonic() || feedSonicFromDecoder(); } else { @@ -133,13 +133,13 @@ import java.nio.ByteBuffer; @Override @Nullable - public Format getOutputFormat() { + public Format getOutputFormat() throws TransformationException { return encoder != null ? encoder.getOutputFormat() : null; } @Override @Nullable - public DecoderInputBuffer getOutputBuffer() { + public DecoderInputBuffer getOutputBuffer() throws TransformationException { if (encoder != null) { encoderOutputBuffer.data = encoder.getOutputBuffer(); if (encoderOutputBuffer.data != null) { @@ -152,7 +152,7 @@ import java.nio.ByteBuffer; } @Override - public void releaseOutputBuffer() { + public void releaseOutputBuffer() throws TransformationException { checkStateNotNull(encoder).releaseOutputBuffer(); } @@ -174,7 +174,7 @@ import java.nio.ByteBuffer; * Attempts to pass decoder output data to the encoder, and returns whether it may be possible to * pass more data immediately by calling this method again. */ - private boolean feedEncoderFromDecoder() { + private boolean feedEncoderFromDecoder() throws TransformationException { if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) { return false; } @@ -203,7 +203,7 @@ import java.nio.ByteBuffer; * Attempts to pass audio processor output data to the encoder, and returns whether it may be * possible to pass more data immediately by calling this method again. */ - private boolean feedEncoderFromSonic() { + private boolean feedEncoderFromSonic() throws TransformationException { if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) { return false; } @@ -226,7 +226,7 @@ import java.nio.ByteBuffer; * Attempts to process decoder output data, and returns whether it may be possible to process more * data immediately by calling this method again. */ - private boolean feedSonicFromDecoder() { + private boolean feedSonicFromDecoder() throws TransformationException { if (drainingSonicForSpeedChange) { if (sonicAudioProcessor.isEnded() && !sonicOutputBuffer.hasRemaining()) { flushSonicAndSetSpeed(currentSpeed); @@ -267,7 +267,7 @@ import java.nio.ByteBuffer; * Feeds as much data as possible between the current position and limit of the specified {@link * ByteBuffer} to the encoder, and advances its position by the number of bytes fed. */ - private void feedEncoder(ByteBuffer inputBuffer) { + private void feedEncoder(ByteBuffer inputBuffer) throws TransformationException { ByteBuffer encoderInputBufferData = checkNotNull(encoderInputBuffer.data); int bufferLimit = inputBuffer.limit(); inputBuffer.limit(min(bufferLimit, inputBuffer.position() + encoderInputBufferData.capacity())); @@ -283,7 +283,7 @@ import java.nio.ByteBuffer; encoder.queueInputBuffer(encoderInputBuffer); } - private void queueEndOfStreamToEncoder() { + private void queueEndOfStreamToEncoder() throws TransformationException { checkState(checkNotNull(encoderInputBuffer.data).position() == 0); encoderInputBuffer.timeUs = nextEncoderInputBufferTimeUs; encoderInputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); 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 index b95bc862a2..b898bb20a7 100644 --- 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 @@ -107,6 +107,7 @@ public final class Codec { private final BufferInfo outputBufferInfo; private final MediaCodec mediaCodec; + private final Format configurationFormat; @Nullable private final Surface inputSurface; private @MonotonicNonNull Format outputFormat; @@ -117,15 +118,36 @@ public final class Codec { private boolean inputStreamEnded; private boolean outputStreamEnded; - /** Creates a {@code Codec} from a configured and started {@link MediaCodec}. */ - public Codec(MediaCodec mediaCodec, @Nullable Surface inputSurface) { + /** + * Creates a {@code Codec} from a configured and started {@link MediaCodec}. + * + * @param mediaCodec The configured and started {@link MediaCodec}. + * @param configurationFormat See {@link #getConfigurationFormat()}. + * @param inputSurface The input {@link Surface} if the {@link MediaCodec} receives input from a + * surface. + */ + public Codec(MediaCodec mediaCodec, Format configurationFormat, @Nullable Surface inputSurface) { this.mediaCodec = mediaCodec; + this.configurationFormat = configurationFormat; this.inputSurface = inputSurface; outputBufferInfo = new BufferInfo(); inputBufferIndex = C.INDEX_UNSET; outputBufferIndex = C.INDEX_UNSET; } + /** + * Returns the {@link Format} used for configuring the codec. + * + *

The configuration {@link Format} is the input {@link Format} used by the {@link + * DecoderFactory} or output {@link Format} used by the {@link EncoderFactory} for selecting and + * configuring the underlying {@link MediaCodec}. + */ + // TODO(b/214012830): Use this to check whether the Format passed to the factory and actual + // configuration format differ to see whether fallback was applied. + public Format getConfigurationFormat() { + return configurationFormat; + } + /** Returns the input {@link Surface}, or null if the input is not a surface. */ @Nullable public Surface getInputSurface() { @@ -137,18 +159,28 @@ public final class Codec { * * @param inputBuffer The buffer where the dequeued buffer data is stored. * @return Whether an input buffer is ready to be used. + * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem. */ @EnsuresNonNullIf(expression = "#1.data", result = true) - public boolean maybeDequeueInputBuffer(DecoderInputBuffer inputBuffer) { + public boolean maybeDequeueInputBuffer(DecoderInputBuffer inputBuffer) + throws TransformationException { if (inputStreamEnded) { return false; } if (inputBufferIndex < 0) { - inputBufferIndex = mediaCodec.dequeueInputBuffer(/* timeoutUs= */ 0); + try { + inputBufferIndex = mediaCodec.dequeueInputBuffer(/* timeoutUs= */ 0); + } catch (RuntimeException e) { + throw createTransformationException(e); + } if (inputBufferIndex < 0) { return false; } - inputBuffer.data = mediaCodec.getInputBuffer(inputBufferIndex); + try { + inputBuffer.data = mediaCodec.getInputBuffer(inputBufferIndex); + } catch (RuntimeException e) { + throw createTransformationException(e); + } inputBuffer.clear(); } checkNotNull(inputBuffer.data); @@ -158,8 +190,13 @@ public final class Codec { /** * Queues an input buffer to the decoder. No buffers may be queued after an {@link * DecoderInputBuffer#isEndOfStream() end of stream} buffer has been queued. + * + * @param inputBuffer The {@link DecoderInputBuffer input buffer}. + * @throws IllegalStateException If called again after an {@link + * DecoderInputBuffer#isEndOfStream() end of stream} buffer has been queued. + * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem. */ - public void queueInputBuffer(DecoderInputBuffer inputBuffer) { + public void queueInputBuffer(DecoderInputBuffer inputBuffer) throws TransformationException { checkState( !inputStreamEnded, "Input buffer can not be queued after the input stream has ended."); @@ -174,32 +211,64 @@ public final class Codec { inputStreamEnded = true; flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM; } - mediaCodec.queueInputBuffer(inputBufferIndex, offset, size, inputBuffer.timeUs, flags); + try { + mediaCodec.queueInputBuffer(inputBufferIndex, offset, size, inputBuffer.timeUs, flags); + } catch (RuntimeException e) { + throw createTransformationException(e); + } inputBufferIndex = C.INDEX_UNSET; inputBuffer.data = null; } - public void signalEndOfInputStream() { - mediaCodec.signalEndOfInputStream(); + /** + * Signals end-of-stream on input to a video encoder. + * + *

This method does not need to be called for audio/video decoders or audio encoders. For these + * the {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag should be set on the last input buffer + * {@link #queueInputBuffer(DecoderInputBuffer) queued}. + * + * @throws IllegalStateException If the codec is not an encoder receiving input from a {@link + * Surface}. + * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem. + */ + public void signalEndOfInputStream() throws TransformationException { + checkState(mediaCodec.getCodecInfo().isEncoder() && inputSurface != null); + try { + mediaCodec.signalEndOfInputStream(); + } catch (RuntimeException e) { + throw createTransformationException(e); + } } - /** Returns the current output format, if available. */ + /** + * Returns the current output format, if available. + * + * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem. + */ @Nullable - public Format getOutputFormat() { + public Format getOutputFormat() throws TransformationException { // 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. */ + /** + * Returns the current output {@link ByteBuffer}, if available. + * + * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem. + */ @Nullable - public ByteBuffer getOutputBuffer() { + public ByteBuffer getOutputBuffer() throws TransformationException { return maybeDequeueAndSetOutputBuffer() ? outputBuffer : null; } - /** Returns the {@link BufferInfo} associated with the current output buffer, if available. */ + /** + * Returns the {@link BufferInfo} associated with the current output buffer, if available. + * + * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem. + */ @Nullable - public BufferInfo getOutputBufferInfo() { + public BufferInfo getOutputBufferInfo() throws TransformationException { return maybeDequeueOutputBuffer() ? outputBufferInfo : null; } @@ -208,8 +277,10 @@ public final class Codec { * *

This should be called after the buffer has been processed. The next output buffer will not * be available until the previous has been released. + * + * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem. */ - public void releaseOutputBuffer() { + public void releaseOutputBuffer() throws TransformationException { releaseOutputBuffer(/* render= */ false); } @@ -221,10 +292,17 @@ public final class Codec { * *

This should be called after the buffer has been processed. The next output buffer will not * be available until the previous has been released. + * + * @param render Whether the buffer needs to be sent to the output {@link Surface}. + * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem. */ - public void releaseOutputBuffer(boolean render) { + public void releaseOutputBuffer(boolean render) throws TransformationException { outputBuffer = null; - mediaCodec.releaseOutputBuffer(outputBufferIndex, render); + try { + mediaCodec.releaseOutputBuffer(outputBufferIndex, render); + } catch (RuntimeException e) { + throw createTransformationException(e); + } outputBufferIndex = C.INDEX_UNSET; } @@ -246,13 +324,18 @@ public final class Codec { * 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. + * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem. */ - private boolean maybeDequeueAndSetOutputBuffer() { + private boolean maybeDequeueAndSetOutputBuffer() throws TransformationException { if (!maybeDequeueOutputBuffer()) { return false; } - outputBuffer = checkNotNull(mediaCodec.getOutputBuffer(outputBufferIndex)); + try { + outputBuffer = checkNotNull(mediaCodec.getOutputBuffer(outputBufferIndex)); + } catch (RuntimeException e) { + throw createTransformationException(e); + } outputBuffer.position(outputBufferInfo.offset); outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); return true; @@ -261,8 +344,10 @@ public final class Codec { /** * 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. + * + * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem. */ - private boolean maybeDequeueOutputBuffer() { + private boolean maybeDequeueOutputBuffer() throws TransformationException { if (outputBufferIndex >= 0) { return true; } @@ -270,7 +355,11 @@ public final class Codec { return false; } - outputBufferIndex = mediaCodec.dequeueOutputBuffer(outputBufferInfo, /* timeoutUs= */ 0); + try { + outputBufferIndex = mediaCodec.dequeueOutputBuffer(outputBufferInfo, /* timeoutUs= */ 0); + } catch (RuntimeException e) { + throw createTransformationException(e); + } if (outputBufferIndex < 0) { if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { outputFormat = getFormat(mediaCodec.getOutputFormat()); @@ -292,6 +381,20 @@ public final class Codec { return true; } + private TransformationException createTransformationException(Exception cause) { + boolean isEncoder = mediaCodec.getCodecInfo().isEncoder(); + boolean isVideo = MimeTypes.isVideo(configurationFormat.sampleMimeType); + String componentName = (isVideo ? "Video" : "Audio") + (isEncoder ? "Encoder" : "Decoder"); + return TransformationException.createForCodec( + cause, + componentName, + configurationFormat, + mediaCodec.getName(), + isEncoder + ? TransformationException.ERROR_CODE_ENCODING_FAILED + : TransformationException.ERROR_CODE_DECODING_FAILED); + } + private static Format getFormat(MediaFormat mediaFormat) { ImmutableList.Builder csdBuffers = new ImmutableList.Builder<>(); int csdIndex = 0; 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 index 80c43bd818..081e502ccf 100644 --- 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 @@ -138,12 +138,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; if (inputSurface != null) { inputSurface.release(); } + @Nullable String mediaCodecName = null; if (mediaCodec != null) { + mediaCodecName = mediaCodec.getName(); mediaCodec.release(); } - throw createTransformationException(e, format, isVideo, isDecoder); + throw createTransformationException(e, format, isVideo, isDecoder, mediaCodecName); } - return new Codec(mediaCodec, inputSurface); + return new Codec(mediaCodec, format, inputSurface); } private static void configureCodec( @@ -167,13 +169,18 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } private static TransformationException createTransformationException( - Exception cause, Format format, boolean isVideo, boolean isDecoder) { + Exception cause, + Format format, + boolean isVideo, + boolean isDecoder, + @Nullable String mediaCodecName) { String componentName = (isVideo ? "Video" : "Audio") + (isDecoder ? "Decoder" : "Encoder"); if (cause instanceof IOException || cause instanceof MediaCodec.CodecException) { return TransformationException.createForCodec( cause, componentName, format, + mediaCodecName, isDecoder ? TransformationException.ERROR_CODE_DECODER_INIT_FAILED : TransformationException.ERROR_CODE_ENCODER_INIT_FAILED); @@ -183,6 +190,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; cause, componentName, format, + mediaCodecName, isDecoder ? TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED : TransformationException.ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED); 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 62c94ee6c2..ebf7d1e0de 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 @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; /** Returns a buffer if the pipeline is ready to accept input, and {@code null} otherwise. */ @Nullable - DecoderInputBuffer dequeueInputBuffer(); + DecoderInputBuffer dequeueInputBuffer() throws TransformationException; /** * Informs the pipeline that its input buffer contains new input. @@ -37,7 +37,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; *

Should be called after filling the input buffer from {@link #dequeueInputBuffer()} with new * input. */ - void queueInputBuffer(); + void queueInputBuffer() throws TransformationException; /** * Processes the input data and returns whether more data can be processed by calling this method @@ -47,18 +47,18 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; /** Returns the output format of the pipeline if available, and {@code null} otherwise. */ @Nullable - Format getOutputFormat(); + Format getOutputFormat() throws TransformationException; /** Returns an output buffer if the pipeline has produced output, and {@code null} otherwise */ @Nullable - DecoderInputBuffer getOutputBuffer(); + DecoderInputBuffer getOutputBuffer() throws TransformationException; /** * Releases the pipeline's output buffer. * *

Should be called when the output buffer from {@link #getOutputBuffer()} is no longer needed. */ - void releaseOutputBuffer(); + void releaseOutputBuffer() throws TransformationException; /** Returns whether the pipeline has ended. */ boolean isEnded(); 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 ce93e13c39..c74901730a 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 @@ -21,6 +21,7 @@ import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE_USE; +import android.media.MediaCodec; import android.os.SystemClock; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -216,14 +217,25 @@ public final class TransformationException extends 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 configurationFormat The {@link Format} used for configuring the decoder/encoder. + * @param mediaCodecName The name of the {@link MediaCodec} used, if known. * @param errorCode See {@link #errorCode}. * @return The created instance. */ public static TransformationException createForCodec( - Throwable cause, String componentName, Format format, int errorCode) { + Throwable cause, + String componentName, + Format configurationFormat, + @Nullable String mediaCodecName, + int errorCode) { return new TransformationException( - componentName + " error, format = " + format, cause, errorCode); + componentName + + " error, format = " + + configurationFormat + + ", mediaCodecName=" + + mediaCodecName, + cause, + errorCode); } /** 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 1e48226da6..eecb0cae62 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 @@ -139,7 +139,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; protected abstract boolean ensureConfigured() throws TransformationException; @RequiresNonNull({"samplePipeline", "#1.data"}) - protected void maybeQueueSampleToPipeline(DecoderInputBuffer inputBuffer) { + protected void maybeQueueSampleToPipeline(DecoderInputBuffer inputBuffer) + throws TransformationException { samplePipeline.queueInputBuffer(); } @@ -147,9 +148,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * Attempts to write sample pipeline output data to the muxer. * * @return Whether it may be possible to write more data immediately by calling this method again. + * @throws Muxer.MuxerException If a muxing problem occurs. + * @throws TransformationException If a {@link SamplePipeline} problem occurs. */ @RequiresNonNull("samplePipeline") - private boolean feedMuxerFromPipeline() throws Muxer.MuxerException { + private boolean feedMuxerFromPipeline() throws Muxer.MuxerException, TransformationException { if (!muxerWrapperTrackAdded) { @Nullable Format samplePipelineOutputFormat = samplePipeline.getOutputFormat(); if (samplePipelineOutputFormat == null) { @@ -185,9 +188,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * Attempts to read input data and pass the input data to the sample pipeline. * * @return Whether it may be possible to read more data immediately by calling this method again. + * @throws TransformationException If a {@link SamplePipeline} problem occurs. */ @RequiresNonNull("samplePipeline") - private boolean feedPipelineFromInput() { + private boolean feedPipelineFromInput() throws TransformationException { @Nullable DecoderInputBuffer samplePipelineInputBuffer = samplePipeline.dequeueInputBuffer(); if (samplePipelineInputBuffer == null) { return false; 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 a89b1cb81f..82968f888f 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 @@ -122,10 +122,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Queues the input buffer to the sample pipeline unless it should be dropped because of slow * motion flattening. + * + * @param inputBuffer The {@link DecoderInputBuffer}. + * @throws TransformationException If a {@link SamplePipeline} problem occurs. */ @Override @RequiresNonNull({"samplePipeline", "#1.data"}) - protected void maybeQueueSampleToPipeline(DecoderInputBuffer inputBuffer) { + protected void maybeQueueSampleToPipeline(DecoderInputBuffer inputBuffer) + throws TransformationException { ByteBuffer data = inputBuffer.data; boolean shouldDropSample = sefSlowMotionFlattener != null && sefSlowMotionFlattener.dropOrTransformSample(inputBuffer); 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 c5f2f5d2a1..38e07e9d5b 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 @@ -131,12 +131,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override @Nullable - public DecoderInputBuffer dequeueInputBuffer() { + public DecoderInputBuffer dequeueInputBuffer() throws TransformationException { return decoder.maybeDequeueInputBuffer(decoderInputBuffer) ? decoderInputBuffer : null; } @Override - public void queueInputBuffer() { + public void queueInputBuffer() throws TransformationException { decoder.queueInputBuffer(decoderInputBuffer); } @@ -217,7 +217,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override @Nullable - public Format getOutputFormat() { + public Format getOutputFormat() throws TransformationException { Format format = encoder.getOutputFormat(); return format == null ? null @@ -226,7 +226,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override @Nullable - public DecoderInputBuffer getOutputBuffer() { + public DecoderInputBuffer getOutputBuffer() throws TransformationException { encoderOutputBuffer.data = encoder.getOutputBuffer(); if (encoderOutputBuffer.data == null) { return null; @@ -238,7 +238,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @Override - public void releaseOutputBuffer() { + public void releaseOutputBuffer() throws TransformationException { encoder.releaseOutputBuffer(); } From a36e0cf2553da5679bf97d56e58413dd1c4c48b1 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 13 Jan 2022 16:56:59 +0000 Subject: [PATCH 23/28] Promote MappedTrackInfo.RendererSupport IntDef to public This is referred to from the public API surface, so it should also be public: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/trackselection/MappingTrackSelector.MappedTrackInfo.html#getRendererSupport(int) #minor-release PiperOrigin-RevId: 421578232 --- .../android/exoplayer2/trackselection/MappingTrackSelector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..2bbe974c47 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 @@ -67,7 +67,7 @@ public abstract class MappingTrackSelector extends TrackSelector { RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS, RENDERER_SUPPORT_PLAYABLE_TRACKS }) - @interface RendererSupport {} + public @interface RendererSupport {} /** The renderer does not have any associated tracks. */ public static final int RENDERER_SUPPORT_NO_TRACKS = 0; /** From 66c272c9b05cf15b27e0f23bc377a46fc7eee4c6 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 14 Jan 2022 00:08:10 +0000 Subject: [PATCH 24/28] Reword javadoc of TracksInfo.isTypeSupportedOrEmpty The existing wording would be correct if prefixed with "Returns false if [...]", but it seems confusing to a document a boolean method in terms the condition it returns false - so I reworded it in terms of when it returns true. #minor-release PiperOrigin-RevId: 421682584 --- .../java/com/google/android/exoplayer2/TracksInfo.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/TracksInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/TracksInfo.java index 8f723a1859..328f8a4080 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/TracksInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/TracksInfo.java @@ -225,7 +225,10 @@ public final class TracksInfo implements Bundleable { return trackGroupInfos; } - /** Returns if there is at least one track of type {@code trackType} but none are supported. */ + /** + * Returns true if at least one track of type {@code trackType} is {@link + * TrackGroupInfo#isTrackSupported(int) supported}, or there are no tracks of this type. + */ public boolean isTypeSupportedOrEmpty(@C.TrackType int trackType) { boolean supported = true; for (int i = 0; i < trackGroupInfos.size(); i++) { @@ -240,7 +243,7 @@ public final class TracksInfo implements Bundleable { return supported; } - /** Returns if at least one track of the type {@code trackType} is selected for playback. */ + /** Returns true if at least one track of the type {@code trackType} is selected for playback. */ public boolean isTypeSelected(@C.TrackType int trackType) { for (int i = 0; i < trackGroupInfos.size(); i++) { TrackGroupInfo trackGroupInfo = trackGroupInfos.get(i); From a01ead028384f77c24dad15b3b89eefc1c1d5bbc Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 14 Jan 2022 11:18:19 +0000 Subject: [PATCH 25/28] Fix deprecation suppression in RendererCapabilities This string is case-sensitive. PiperOrigin-RevId: 421781437 --- .../com/google/android/exoplayer2/RendererCapabilities.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..79938f6946 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 @@ -25,7 +25,7 @@ import java.lang.annotation.RetentionPolicy; public interface RendererCapabilities { /** @deprecated Use {@link C.FormatSupport} instead. */ - @SuppressWarnings("Deprecation") + @SuppressWarnings("deprecation") @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({ From f673676c6d9d8ce7c4f944c12d134fbab319721d Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 14 Jan 2022 11:19:59 +0000 Subject: [PATCH 26/28] Specify the video ID used in the Widevine DASH samples in the demo app This value is the default used by widevine_test at proxy.uat.widevine.com, but it's not easy to find that info so it's clearer to document it explicitly here for consistency with the "policy tests" section below where all the URLs contain a video_id parameter. Issue: google/ExoPlayer#9852 PiperOrigin-RevId: 421781663 --- demos/main/src/main/assets/media.exolist.json | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index d1db406667..0b479ff6d5 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -35,31 +35,31 @@ "name": "HD (cenc)", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", "drm_scheme": "widevine", - "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" }, { "name": "UHD (cenc)", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd", "drm_scheme": "widevine", - "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" }, { "name": "HD (cbcs)", "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd", "drm_scheme": "widevine", - "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" }, { "name": "UHD (cbcs)", "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd", "drm_scheme": "widevine", - "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" }, { "name": "Secure -> Clear -> Secure (cenc)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd", "drm_scheme": "widevine", - "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test", "drm_session_for_clear_content": true } ] @@ -71,25 +71,25 @@ "name": "HD (cenc, full-sample)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd", "drm_scheme": "widevine", - "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" }, { "name": "UHD (cenc, full-sample)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd", "drm_scheme": "widevine", - "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" }, { "name": "HD (cenc, sub-sample)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd", "drm_scheme": "widevine", - "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" }, { "name": "UHD (cenc, sub-sample)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd", "drm_scheme": "widevine", - "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" } ] }, @@ -100,13 +100,13 @@ "name": "HD (cenc)", "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", "drm_scheme": "widevine", - "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" }, { "name": "UHD (cenc)", "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd", "drm_scheme": "widevine", - "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" } ] }, From f216fa20421d39b225736885be9dcb8c565dc39a Mon Sep 17 00:00:00 2001 From: huangdarwin Date: Fri, 14 Jan 2022 12:27:18 +0000 Subject: [PATCH 27/28] Transformer GL: Clarify variables and comments. Simplifying and clarifying variables, and adding comments. Tested by confirming demo-gl and demo-transformer both correctly display videos PiperOrigin-RevId: 421792079 --- .../gldemo/BitmapOverlayVideoProcessor.java | 18 ++------------- .../android/exoplayer2/util/GlUtil.java | 23 +++++++++++++++++++ .../main/assets/shaders/vertex_shader.glsl | 4 ++-- .../exoplayer2/transformer/FrameEditor.java | 19 +++------------ .../transformer/TransformationRequest.java | 7 ++++++ .../transformer/VideoSamplePipeline.java | 21 +++++++++-------- 6 files changed, 49 insertions(+), 43 deletions(-) diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java index 1d927fff57..1294990ec5 100644 --- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java @@ -86,23 +86,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; throw new IllegalStateException(e); } program.setBufferAttribute( - "a_position", - new float[] { - -1, -1, 0, 1, - 1, -1, 0, 1, - -1, 1, 0, 1, - 1, 1, 0, 1 - }, - 4); + "a_position", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); program.setBufferAttribute( - "a_texcoord", - new float[] { - 0, 0, 0, 1, - 1, 0, 0, 1, - 0, 1, 0, 1, - 1, 1, 0, 1 - }, - 4); + "a_texcoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); GLES20.glGenTextures(1, textures, 0); GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java index 427e6bf6f7..6bdb8d17f2 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java @@ -202,6 +202,9 @@ public final class GlUtil { /** Whether to throw a {@link GlException} in case of an OpenGL error. */ public static boolean glAssertionsEnabled = false; + /** Number of vertices in a rectangle. */ + public static final int RECTANGLE_VERTICES_COUNT = 4; + private static final String TAG = "GlUtil"; private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content"; @@ -210,6 +213,26 @@ public final class GlUtil { /** Class only contains static methods. */ private GlUtil() {} + /** Bounds of normalized device coordinates, commonly used for defining viewport boundaries. */ + public static float[] getNormalizedCoordinateBounds() { + return new float[] { + -1, -1, 0, 1, + 1, -1, 0, 1, + -1, 1, 0, 1, + 1, 1, 0, 1 + }; + } + + /** Typical bounds used for sampling from textures. */ + public static float[] getTextureCoordinateBounds() { + return new float[] { + 0, 0, 0, 1, + 1, 0, 0, 1, + 0, 1, 0, 1, + 1, 1, 0, 1 + }; + } + /** * Returns whether creating a GL context with {@value #EXTENSION_PROTECTED_CONTENT} is possible. * If {@code true}, the device supports a protected output path for DRM content when using GL. diff --git a/library/transformer/src/main/assets/shaders/vertex_shader.glsl b/library/transformer/src/main/assets/shaders/vertex_shader.glsl index 3fd3e553fc..4f5e883390 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 aPosition; +attribute vec4 aFramePosition; attribute vec4 aTexCoords; uniform mat4 uTexTransform; uniform mat4 uTransformationMatrix; varying vec2 vTexCoords; void main() { - gl_Position = uTransformationMatrix * aPosition; + gl_Position = uTransformationMatrix * aFramePosition; 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 29e0d50a8e..f7a3de2bde 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 @@ -131,24 +131,11 @@ import java.util.concurrent.atomic.AtomicInteger; GlUtil.Program glProgram = new GlUtil.Program(context, VERTEX_SHADER_FILE_PATH, FRAGMENT_SHADER_FILE_PATH); + // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. glProgram.setBufferAttribute( - "aPosition", - 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); + "aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); glProgram.setBufferAttribute( - "aTexCoords", - 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); + "aTexCoords", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); glProgram.setSamplerTexIdUniform("uTexSampler", textureId, /* unit= */ 0); float[] transformationMatrixArray = getGlMatrixArray(transformationMatrix); 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 ce614235b5..22a7afabb1 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 @@ -61,6 +61,9 @@ public final class TransformationRequest { *

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

The video dimensions will be on the x axis, from -aspectRatio to aspectRatio, and on the y + * axis, from -1 to 1. + * *

For now, resolution will not be affected by this method. * * @param transformationMatrix The transformation to apply to video frames. @@ -71,6 +74,10 @@ public final class TransformationRequest { // 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). + + // TODO(b/213198690): Consider changing how transformationMatrix is applied, so that + // dimensions will be from -1 to 1 on both x and y axes, but transformations will be applied + // in a predictable manner. this.transformationMatrix = transformationMatrix; return this; } 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 38e07e9d5b..0ad61f8dee 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 @@ -63,12 +63,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // Scale width and height to desired transformationRequest.outputHeight, preserving aspect // ratio. // TODO(internal b/209781577): Think about which edge length should be set for portrait videos. - float inputAspectRatio = (float) inputFormat.width / inputFormat.height; + float inputFormatAspectRatio = (float) inputFormat.width / inputFormat.height; int outputWidth = inputFormat.width; int outputHeight = inputFormat.height; if (transformationRequest.outputHeight != C.LENGTH_UNSET && transformationRequest.outputHeight != inputFormat.height) { - outputWidth = Math.round(inputAspectRatio * transformationRequest.outputHeight); + outputWidth = Math.round(inputFormatAspectRatio * transformationRequest.outputHeight); outputHeight = transformationRequest.outputHeight; } @@ -82,14 +82,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } else { outputRotationDegrees = inputFormat.rotationDegrees; } - if ((inputFormat.rotationDegrees % 180) != 0) { - inputAspectRatio = 1.0f / inputAspectRatio; - } + float displayAspectRatio = + (inputFormat.rotationDegrees % 180) == 0 + ? inputFormatAspectRatio + : 1.0f / inputFormatAspectRatio; - // Scale frames by input aspect ratio, to account for FrameEditor normalized device coordinates - // (-1 to 1) and preserve frame relative dimensions during transformations (ex. rotations). - transformationRequest.transformationMatrix.preScale(inputAspectRatio, 1); - transformationRequest.transformationMatrix.postScale(1.0f / inputAspectRatio, 1); + // Scale frames by input aspect ratio, to account for FrameEditor's square normalized device + // coordinates (-1 to 1) and preserve frame relative dimensions during transformations + // (ex. rotations). After this scaling, transformationMatrix operations operate on a rectangle + // for x from -displayAspectRatio to displayAspectRatio, and y from -1 to 1 + transformationRequest.transformationMatrix.preScale(displayAspectRatio, 1); + transformationRequest.transformationMatrix.postScale(1.0f / displayAspectRatio, 1); // The decoder rotates videos to their intended display orientation. The frameEditor rotates // them back for improved encoder compatibility. From 7777ebd173dad341aadbb476644c1e025376c4c8 Mon Sep 17 00:00:00 2001 From: Aurelien Drouet Date: Thu, 18 Nov 2021 12:15:06 +0900 Subject: [PATCH 28/28] fix: unused parameter chunkExtractorFactory --- .../android/exoplayer2/source/dash/DefaultDashChunkSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index aaca62086a..0bdc49d76d 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -217,7 +217,7 @@ public class DefaultDashChunkSource implements DashChunkSource { periodDurationUs, representation, selectedBaseUrl != null ? selectedBaseUrl : representation.baseUrls.get(0), - BundledChunkExtractor.FACTORY.createProgressiveMediaExtractor( + chunkExtractorFactory.createProgressiveMediaExtractor( trackType, representation.format, enableEventMessageTrack,