diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8adfa66e6b..e080c7bc2b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,11 +3,17 @@ ### dev-v2 (not yet released) * Core Library: + * Fix track selection issue where a mixture of non-empty and empty track + overrides is not applied correctly + ([#9649](https://github.com/google/ExoPlayer/issues/9649). * Add protected method `DefaultRenderersFactory.getCodecAdapterFactory()` so that subclasses of `DefaultRenderersFactory` that override `buildVideoRenderers()` or `buildAudioRenderers()` can access the codec adapter factory and pass it to `MediaCodecRenderer` instances they create. +* Extractors: + * WAV: Add support for RF64 streams + ([#9543](https://github.com/google/ExoPlayer/issues/9543). * RTSP * Provide a client API to override the `SocketFactory` used for any server connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)). diff --git a/SECURITY.md b/SECURITY.md index 4ec8ec4a3b..7a1b043f8b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,8 +1,9 @@ # Security policy # -To report a security issue, please email exoplayer-support+security@google.com -with a description of the issue, the steps you took to create the issue, -affected versions, and, if known, mitigations for the issue. Our vulnerability -management team will respond within 3 working days of your email. If the issue -is confirmed as a vulnerability, we will open a Security Advisory. This project -follows a 90 day disclosure timeline. +To report a security issue, please email +android-media-support+security@google.com with a description of the issue, the +steps you took to create the issue, affected versions, and, if known, +mitigations for the issue. Our vulnerability management team will respond within +3 working days of your email. If the issue is confirmed as a vulnerability, we +will open a Security Advisory. This project follows a 90 day disclosure +timeline. diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java index 5b513e93f5..106b620ea1 100644 --- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java @@ -179,8 +179,7 @@ public final class MainActivity extends Activity { player.play(); VideoProcessingGLSurfaceView videoProcessingGLSurfaceView = Assertions.checkNotNull(this.videoProcessingGLSurfaceView); - videoProcessingGLSurfaceView.setVideoComponent( - Assertions.checkNotNull(player.getVideoComponent())); + videoProcessingGLSurfaceView.setPlayer(player); Assertions.checkNotNull(playerView).setPlayer(player); player.addAnalyticsListener(new EventLogger(/* trackSelector= */ null)); this.player = player; @@ -188,9 +187,9 @@ public final class MainActivity extends Activity { private void releasePlayer() { Assertions.checkNotNull(playerView).setPlayer(null); + Assertions.checkNotNull(videoProcessingGLSurfaceView).setPlayer(null); if (player != null) { player.release(); - Assertions.checkNotNull(videoProcessingGLSurfaceView).setVideoComponent(null); player = null; } } diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java index 4cc0813dab..e2796b0370 100644 --- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java @@ -73,7 +73,7 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView { @Nullable private SurfaceTexture surfaceTexture; @Nullable private Surface surface; - @Nullable private ExoPlayer.VideoComponent videoComponent; + @Nullable private ExoPlayer player; /** * Creates a new instance. Pass {@code true} for {@code requireSecureContext} if the {@link @@ -147,25 +147,24 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView { } /** - * Attaches or detaches (if {@code newVideoComponent} is {@code null}) this view from the video - * component of the player. + * Attaches or detaches (if {@code player} is {@code null}) this view from the player. * - * @param newVideoComponent The new video component, or {@code null} to detach this view. + * @param player The new player, or {@code null} to detach this view. */ - public void setVideoComponent(@Nullable ExoPlayer.VideoComponent newVideoComponent) { - if (newVideoComponent == videoComponent) { + public void setPlayer(@Nullable ExoPlayer player) { + if (player == this.player) { return; } - if (videoComponent != null) { + if (this.player != null) { if (surface != null) { - videoComponent.clearVideoSurface(surface); + this.player.clearVideoSurface(surface); } - videoComponent.clearVideoFrameMetadataListener(renderer); + this.player.clearVideoFrameMetadataListener(renderer); } - videoComponent = newVideoComponent; - if (videoComponent != null) { - videoComponent.setVideoFrameMetadataListener(renderer); - videoComponent.setVideoSurface(surface); + this.player = player; + if (this.player != null) { + this.player.setVideoFrameMetadataListener(renderer); + this.player.setVideoSurface(surface); } } @@ -176,8 +175,8 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView { mainHandler.post( () -> { if (surface != null) { - if (videoComponent != null) { - videoComponent.setVideoSurface(null); + if (player != null) { + player.setVideoSurface(null); } releaseSurface(surfaceTexture, surface); surfaceTexture = null; @@ -194,8 +193,8 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView { this.surfaceTexture = surfaceTexture; this.surface = new Surface(surfaceTexture); releaseSurface(oldSurfaceTexture, oldSurface); - if (videoComponent != null) { - videoComponent.setVideoSurface(surface); + if (player != null) { + player.setVideoSurface(surface); } }); } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java index d3579c8c35..afc155bfc3 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java @@ -23,11 +23,13 @@ import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaItem.ClippingConfiguration; +import com.google.android.exoplayer2.MediaItem.SubtitleConfiguration; import com.google.android.exoplayer2.MediaMetadata; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -118,36 +120,46 @@ public class IntentUtil { @Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix); @Nullable String title = intent.getStringExtra(TITLE_EXTRA + extrasKeySuffix); @Nullable String adTagUri = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix); + @Nullable + SubtitleConfiguration subtitleConfiguration = + createSubtitleConfiguration(intent, extrasKeySuffix); MediaItem.Builder builder = new MediaItem.Builder() .setUri(uri) .setMimeType(mimeType) .setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build()) - .setSubtitles(createSubtitlesFromIntent(intent, extrasKeySuffix)) - .setClipStartPositionMs( - intent.getLongExtra(CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, 0)) - .setClipEndPositionMs( - intent.getLongExtra( - CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, C.TIME_END_OF_SOURCE)); + .setClippingConfiguration( + new ClippingConfiguration.Builder() + .setStartPositionMs( + intent.getLongExtra(CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, 0)) + .setEndPositionMs( + intent.getLongExtra( + CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, C.TIME_END_OF_SOURCE)) + .build()); if (adTagUri != null) { builder.setAdsConfiguration( new MediaItem.AdsConfiguration.Builder(Uri.parse(adTagUri)).build()); } + if (subtitleConfiguration != null) { + builder.setSubtitleConfigurations(ImmutableList.of(subtitleConfiguration)); + } return populateDrmPropertiesFromIntent(builder, intent, extrasKeySuffix).build(); } - private static List createSubtitlesFromIntent( + @Nullable + private static MediaItem.SubtitleConfiguration createSubtitleConfiguration( Intent intent, String extrasKeySuffix) { if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) { - return Collections.emptyList(); + return null; } - return Collections.singletonList( - new MediaItem.Subtitle( - Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)), - checkNotNull(intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix)), - intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix), - C.SELECTION_FLAG_DEFAULT)); + return new MediaItem.SubtitleConfiguration.Builder( + Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix))) + .setMimeType( + checkNotNull(intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix))) + .setLanguage(intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix)) + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build(); } private static MediaItem.Builder populateDrmPropertiesFromIntent( diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index ca9fd45d42..302138d947 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -269,7 +269,8 @@ public class PlayerActivity extends AppCompatActivity trackSelector = new DefaultTrackSelector(/* context= */ this); lastSeenTracksInfo = TracksInfo.EMPTY; player = - new ExoPlayer.Builder(/* context= */ this, renderersFactory) + new ExoPlayer.Builder(/* context= */ this) + .setRenderersFactory(renderersFactory) .setMediaSourceFactory(mediaSourceFactory) .setTrackSelector(trackSelector) .build(); diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 6cfe215a78..b79a7a62ca 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -43,8 +43,8 @@ import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaItem.ClippingConfiguration; import com.google.android.exoplayer2.MediaMetadata; -import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.upstream.DataSource; @@ -53,6 +53,7 @@ import com.google.android.exoplayer2.upstream.DataSourceUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.io.InputStream; @@ -327,8 +328,7 @@ public class SampleChooserActivity extends AppCompatActivity reader.nextString(); // Ignore. break; default: - throw ParserException.createForMalformedManifest( - "Unsupported name: " + name, /* cause= */ null); + throw new IOException("Unsupported name: " + name, /* cause= */ null); } } reader.endObject(); @@ -351,6 +351,8 @@ public class SampleChooserActivity extends AppCompatActivity boolean drmSessionForClearContent = false; boolean drmMultiSession = false; boolean drmForceDefaultLicenseUri = false; + MediaItem.ClippingConfiguration.Builder clippingConfiguration = + new ClippingConfiguration.Builder(); MediaItem.Builder mediaItem = new MediaItem.Builder(); reader.beginObject(); @@ -367,10 +369,10 @@ public class SampleChooserActivity extends AppCompatActivity extension = reader.nextString(); break; case "clip_start_position_ms": - mediaItem.setClipStartPositionMs(reader.nextLong()); + clippingConfiguration.setStartPositionMs(reader.nextLong()); break; case "clip_end_position_ms": - mediaItem.setClipEndPositionMs(reader.nextLong()); + clippingConfiguration.setEndPositionMs(reader.nextLong()); break; case "ad_tag_uri": mediaItem.setAdsConfiguration( @@ -420,8 +422,7 @@ public class SampleChooserActivity extends AppCompatActivity reader.endArray(); break; default: - throw ParserException.createForMalformedManifest( - "Unsupported attribute name: " + name, /* cause= */ null); + throw new IOException("Unsupported attribute name: " + name, /* cause= */ null); } } reader.endObject(); @@ -439,7 +440,8 @@ public class SampleChooserActivity extends AppCompatActivity mediaItem .setUri(uri) .setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build()) - .setMimeType(adaptiveMimeType); + .setMimeType(adaptiveMimeType) + .setClippingConfiguration(clippingConfiguration.build()); if (drmUuid != null) { mediaItem.setDrmConfiguration( new MediaItem.DrmConfiguration.Builder(drmUuid) @@ -463,13 +465,15 @@ public class SampleChooserActivity extends AppCompatActivity "drm_uuid is required if drm_force_default_license_uri is set."); } if (subtitleUri != null) { - MediaItem.Subtitle subtitle = - new MediaItem.Subtitle( - subtitleUri, - checkNotNull( - subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."), - subtitleLanguage); - mediaItem.setSubtitles(Collections.singletonList(subtitle)); + MediaItem.SubtitleConfiguration subtitleConfiguration = + new MediaItem.SubtitleConfiguration.Builder(subtitleUri) + .setMimeType( + checkNotNull( + subtitleMimeType, + "subtitle_mime_type is required if subtitle_uri is set.")) + .setLanguage(subtitleLanguage) + .build(); + mediaItem.setSubtitleConfigurations(ImmutableList.of(subtitleConfiguration)); } return new PlaylistHolder(title, Collections.singletonList(mediaItem.build())); } diff --git a/docs/_data/locale.yml b/docs/_data/locale.yml index 3871545994..c3fc356594 100644 --- a/docs/_data/locale.yml +++ b/docs/_data/locale.yml @@ -18,7 +18,7 @@ en: &EN FOLLOW_US : "Follow us on [NAME]." EMAIL_ME : "Send me Email." EMAIL_US : "Send us Email." - COPYRIGHT_DATES : "2019" + COPYRIGHT_DATES : "2021" en-GB: <<: *EN @@ -49,7 +49,7 @@ zh-Hans: &ZH_HANS FOLLOW_US : "在 [NAME] 上关注我们。" EMAIL_ME : "给我发邮件。" EMAIL_US : "给我们发邮件。" - COPYRIGHT_DATES : "2019" + COPYRIGHT_DATES : "2021" zh: <<: *ZH_HANS @@ -78,7 +78,7 @@ zh-Hant: &ZH_HANT FOLLOW_US : "在 [NAME] 上關注我們。" EMAIL_ME : "給我發郵件。" EMAIL_US : "給我們發郵件。" - COPYRIGHT_DATES : "2019" + COPYRIGHT_DATES : "2021" zh-TW: <<: *ZH_HANT @@ -105,7 +105,7 @@ ko: &KO FOLLOW_US : "[NAME]에서 팔로우하기" EMAIL_ME : "이메일 보내기" EMAIL_US : "이메일 보내기" - COPYRIGHT_DATES : "2019" + COPYRIGHT_DATES : "2021" ko-KR: <<: *KO diff --git a/docs/index.md b/docs/index.md index 4a512678e1..b28743192f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,6 @@ --- layout: article --- -The Android media team is interested in your experiences with the Android media -APIs and developer resources. Please provide your feedback by -[completing this short survey](https://goo.gle/media-survey-6). -{:.info} ExoPlayer is an application level media player for Android. It provides an alternative to Android’s MediaPlayer API for playing audio and video both diff --git a/docs/playlists.md b/docs/playlists.md index e17e6ef452..d897e14e26 100644 --- a/docs/playlists.md +++ b/docs/playlists.md @@ -27,7 +27,7 @@ H264 and VP9 videos). They may even be of different types (e.g., it’s fine for playlist to contain both videos and audio only streams). It's allowed to use the same `MediaItem` multiple times within a playlist. -## Modifying the playlist ## +## Modifying the playlist It's possible to dynamically modify a playlist by adding, moving and removing media items. This can be done both before and during playback by calling the @@ -63,13 +63,60 @@ currently playing `MediaItem` is removed, the player will automatically move to playing the first remaining successor, or transition to the ended state if no such successor exists. -## Querying the playlist ## +## Querying the playlist The playlist can be queried using `Player.getMediaItemCount` and `Player.getMediaItemAt`. The currently playing media item can be queried -by calling `Player.getCurrentMediaItem`. +by calling `Player.getCurrentMediaItem`. There are also other convenience +methods like `Player.hasNextMediaItem` or `Player.getNextMediaItemIndex` to +simplify navigation in the playlist. -## Identifying playlist items ## +## Repeat modes + +The player supports 3 repeat modes that can be set at any time with +`Player.setRepeatMode`: + +* `Player.REPEAT_MODE_OFF`: The playlist isn't repeated and the player will + transition to `Player.STATE_ENDED` once the last item in the playlist has + been played. +* `Player.REPEAT_MODE_ONE`: The current item is repeated in an endless loop. + Methods like `Player.seekToNextMediaItem` will ignore this and seek to the + next item in the list, which will then be repeated in an endless loop. +* `Player.REPEAT_MODE_ALL`: The entire playlist is repeated in an endless loop. + +## Shuffle mode + +Shuffle mode can be enabled or disabled at any time with +`Player.setShuffleModeEnabled`. When in shuffle mode, the player will play the +playlist in a precomputed, randomized order. All items will be played once and +the shuffle mode can also be combined with `Player.REPEAT_MODE_ALL` to repeat +the same randomized order in an endless loop. When shuffle mode is turned off, +playback continues from the current item at its original position in the +playlist. + +Note that the indices as returned by methods like +`Player.getCurrentMediaItemIndex` always refer to the original, unshuffled +order. Similarly, `Player.seekToNextMediaItem` will not play the item at +`player.getCurrentMediaItemIndex() + 1`, but the next item according to the +shuffle order. Inserting new items in the playlist or removing items will keep +the existing shuffled order unchanged as far as possible. + +### Setting a custom shuffle order + +By default the player supports shuffling by using the `DefaultShuffleOrder`. +This can be customized by providing a custom shuffle order implementation, or by +setting a custom order in the `DefaultShuffleOrder` constructor: + +~~~ +// Set a custom shuffle order for the 5 items currently in the playlist: +exoPlayer.setShuffleOrder( + new DefaultShuffleOrder(new int[] {3, 1, 0, 4, 2}, randomSeed)); +// Enable shuffle mode. +exoPlayer.setShuffleModeEnabled(/* shuffleModeEnabled= */ true); +~~~ +{: .language-java} + +## Identifying playlist items To identify playlist items, `MediaItem.mediaId` can be set when building the item: @@ -84,7 +131,7 @@ MediaItem mediaItem = If an app does not explicitly define a media ID for a media item, the string representation of the URI is used. -## Associating app data with playlist items ## +## Associating app data with playlist items In addition to an ID, each media item can also be configured with a custom tag, which can be any app provided object. One use of custom tags is to attach @@ -98,7 +145,7 @@ MediaItem mediaItem = {: .language-java} -## Detecting when playback transitions to another media item ## +## Detecting when playback transitions to another media item When playback transitions to another media item, or starts repeating the same media item, `Listener.onMediaItemTransition(MediaItem, @@ -132,7 +179,7 @@ public void onMediaItemTransition( ~~~ {: .language-java} -## Detecting when the playlist changes ## +## Detecting when the playlist changes When a media item is added, removed or moved, `Listener.onTimelineChanged(Timeline, @TimelineChangeReason)` is called @@ -158,19 +205,3 @@ timeline update include: * A manifest becoming available after preparing an adaptive media item. * A manifest being updated periodically during playback of a live stream. - -## Setting a custom shuffle order ## - -By default the playlist supports shuffling by using the `DefaultShuffleOrder`. -This can be customized by providing a custom shuffle order implementation: - -~~~ -// Set the custom shuffle order. -exoPlayer.setShuffleOrder(shuffleOrder); -// Enable shuffle mode. -exoPlayer.setShuffleModeEnabled(/* shuffleModeEnabled= */ true); -~~~ -{: .language-java} - -If the repeat mode of the player is set to `REPEAT_MODE_ALL`, the custom shuffle -order is played in an endless loop. diff --git a/docs/track-selection.md b/docs/track-selection.md index 4dd71b5046..abcf2864f8 100644 --- a/docs/track-selection.md +++ b/docs/track-selection.md @@ -3,8 +3,175 @@ title: Track selection --- Track selection determines which of the available media tracks are played by the -player. Track selection is the responsibility of a `TrackSelector`, an instance -of which can be provided whenever an `ExoPlayer` is built. +player. This process is configured by [`TrackSelectionParameters`][], which +support many different options to specify constraints and overrides. + +## Information about existing tracks + +The player needs to prepare the media to know which tracks are available for +selection. You can listen to `Player.Listener.onTracksInfoChanged` to get +notified about changes, which may happen + * When preparation completes + * When the available or selected tracks change + * When the playlist item changes + +~~~ +player.addListener(new Player.Listener() { + @Override + public void onTracksInfoChanged(TracksInfo tracksInfo) { + // Update UI using current TracksInfo. + } +}); +~~~ +{: .language-java} + +You can also retrieve the current `TracksInfo` by calling +`player.getCurrentTracksInfo()`. + +`TracksInfo` contains a list of `TrackGroupInfo`s with information about the +track type, format details, player support and selection status of each +available track. Tracks are grouped together into one `TrackGroup` if they +represent the same content that can be used interchangeably by the player (for +example, all audio tracks of a single language, but with different bitrates). + +~~~ +for (TrackGroupInfo groupInfo : tracksInfo.getTrackGroupInfos()) { + // Group level information. + @C.TrackType int trackType = groupInfo.getTrackType(); + boolean trackInGroupIsSelected = groupInfo.isSelected(); + boolean trackInGroupIsSupported = groupInfo.isSupported(); + TrackGroup group = groupInfo.getTrackGroup(); + for (int i = 0; i < group.length; i++) { + // Individual track information. + boolean isSupported = groupInfo.isTrackSupported(i); + boolean isSelected = groupInfo.isTrackSelected(i); + Format trackFormat = group.getFormat(i); + } +} +~~~ +{: .language-java} + +* A track is 'supported' if the `Player` is able to decode and render its + samples. Note that even if multiple track groups of the same type (for example + multiple audio track groups) are supported, it only means that they are + supported individually and the player is not necessarily able to play them at + the same time. +* A track is 'selected' if the track selector chose this track for playback + using the current `TrackSelectionParameters`. If multiple tracks within one + track group are selected, the player uses these tracks for adaptive playback + (for example, multiple video tracks with different bitrates). Note that only + one of these tracks will be played at any one time. If you want to be notified + of in-playback changes to the adaptive video track you can listen to + `Player.Listener.onVideoSizeChanged`. + +## Modifying track selection parameters + +The selection process can be configured by setting `TrackSelectionParameters` on +the `Player` with `Player.setTrackSelectionParameters`. These updates can be +done before and during playback. In most cases, it's advisable to obtain the +current parameters and only modify the required aspects with the +`TrackSelectionParameters.Builder`. The builder class also allows chaining to +specify multiple options with one command: + +~~~ +player.setTrackSelectionParameters( + player.getTrackSelectionParameters() + .buildUpon() + .setMaxVideoSizeSd() + .setPreferredAudioLanguage("hu") + .build()); +~~~ +{: .language-java} + +### Constraint based track selection + +Most options in `TrackSelectionParameters` allow you to specify constraints, +which are independent of the tracks that are actually available. Typical +constraints are: + + * Maximum or minimum video width, height, frame rate, or bitrate. + * Maximum audio channel count or bitrate. + * Preferred MIME types for video or audio. + * Preferred audio languages or role flags. + * Preferred text languages or role flags. + +Note that ExoPlayer already applies sensible defaults for most of these values, +for example restricting video resolution to the display size or preferring the +audio language that matches the user's system Locale setting. + +There are several benefits to using constraint based track selection instead of +specifying specific tracks directly: + +* You can specify constraints before knowing what tracks the media provides. + This allows to immediately select the appropriate tracks for faster startup + time and also simplifies track selection code as you don't have to listen for + changes in the available tracks. +* Constraints can be applied consistently across all items in a playlist. For + example, selecting an audio language based on user preference will + automatically apply to the next playlist item too, whereas overriding a + specific track will only apply to the current playlist item for which the + track exists. + +### Selecting specific tracks + +It's possible to specify specific tracks in `TrackSelectionParameters` that +should be selected for the current set of tracks. Note that a change in the +available tracks, for example when changing items in a playlist, will also +invalidate such a track override. + +The simplest way to specify track overrides is to specify the `TrackGroup` that +should be selected for its track type. For example, you can specify an audio +track group to select this audio group and prevent any other audio track groups +from being selected: + +~~~ +TrackSelectionOverrides overrides = + new TrackSelectionOverrides.Builder() + .setOverrideForType(new TrackSelectionOverride(audioTrackGroup)) + .build(); +player.setTrackSelectionParameters( + player.getTrackSelectionParameters() + .buildUpon().setTrackSelectionOverrides(overrides).build()); +~~~ +{: .language-java} + +### Disabling track types or groups + +Track types, like video, audio or text, can be disabled completely by using +`TrackSelectionParameters.Builder.setDisabledTrackTypes`. This will apply +unconditionally and will also affect other playlist items. + +~~~ +player.setTrackSelectionParameters( + player.getTrackSelectionParameters() + .buildUpon() + .setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_VIDEO)) + .build()); +~~~ +{: .language-java} + +Alternatively, it's possible to prevent the selection of track groups for the +current playlist item only by specifying empty overrides for these groups: + +~~~ +TrackSelectionOverrides overrides = + new TrackSelectionOverrides.Builder() + .addOverride( + new TrackSelectionOverride( + disabledTrackGroup, + /* select no tracks for this group */ ImmutableList.of())) + .build(); +player.setTrackSelectionParameters( + player.getTrackSelectionParameters() + .buildUpon().setTrackSelectionOverrides(overrides).build()); +~~~ +{: .language-java} + +## Customizing the track selector + +Track selection is the responsibility of a `TrackSelector`, an instance +of which can be provided whenever an `ExoPlayer` is built and later obtained +with `ExoPlayer.getTrackSelector()`. ~~~ DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); @@ -16,28 +183,22 @@ ExoPlayer player = {: .language-java} `DefaultTrackSelector` is a flexible `TrackSelector` suitable for most use -cases. When using a `DefaultTrackSelector`, it's possible to control which -tracks it selects by modifying its `Parameters`. This can be done before or -during playback. For example the following code tells the selector to restrict -video track selections to SD, and to select a German audio track if there is -one: +cases. It uses the `TrackSelectionParameters` set in the `Player`, but also +provides some advanced customization options that can be specified in the +`DefaultTrackSelector.ParametersBuilder`: ~~~ trackSelector.setParameters( trackSelector .buildUponParameters() - .setMaxVideoSizeSd() - .setPreferredAudioLanguage("deu")); + .setAllowVideoMixedMimeTypeAdaptiveness(true)); ~~~ {: .language-java} -This is an example of constraint based track selection, in which constraints are -specified without knowledge of the tracks that are actually available. Many -different types of constraint can be specified using `Parameters`. `Parameters` -can also be used to select specific tracks from those that are available. See -the [`DefaultTrackSelector`][], [`Parameters`][] and [`ParametersBuilder`][] -documentation for more details. +### Tunneling -[`Parameters`]: {{ site.exo_sdk }}/trackselection/DefaultTrackSelector.Parameters.html -[`ParametersBuilder`]: {{ site.exo_sdk }}/trackselection/DefaultTrackSelector.ParametersBuilder.html -[`DefaultTrackSelector`]: {{ site.exo_sdk }}/trackselection/DefaultTrackSelector.html +Tunneled playback can be enabled in cases where the combination of renderers and +selected tracks supports it. This can be done by using +`DefaultTrackSelector.ParametersBuilder.setTunnelingEnabled(true)`. + +[`TrackSelectionParameters`]: {{ site.exo_sdk }}/trackselection/TrackSelectionParameters.html diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java index 12ac0fb4e8..502c3b7ba2 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java @@ -88,8 +88,11 @@ public class DefaultMediaItemConverter implements MediaItemConverter { .setMediaId(mediaId != null ? mediaId : MediaItem.DEFAULT_MEDIA_ID) .setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build()) .setTag(media2MediaItem) - .setClipStartPositionMs(startPositionMs) - .setClipEndPositionMs(endPositionMs) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(startPositionMs) + .setEndPositionMs(endPositionMs) + .build()) .build(); } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index cfda09ff0a..751a850fbe 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -1208,8 +1208,7 @@ public final class MediaSessionConnector { @Override public void onStop() { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_STOP)) { - player.stop(); - player.clearMediaItems(); + player.stop(/* reset= */ true); } } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Player.java b/library/common/src/main/java/com/google/android/exoplayer2/Player.java index b6222596a7..406398e39f 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Player.java @@ -2119,7 +2119,7 @@ public interface Player { /** Returns the index of the period currently being played. */ int getCurrentPeriodIndex(); - /** @deprecated Use {@link #getCurrentMediaItem()} instead. */ + /** @deprecated Use {@link #getCurrentMediaItemIndex()} instead. */ @Deprecated int getCurrentWindowIndex(); diff --git a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java index 239dd76b60..c45e0c6981 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java @@ -113,23 +113,23 @@ public final class TrackSelectionOverrides implements Bundleable { } /** - * Forces the selection of {@link #trackIndexes} for a {@link TrackGroup}. + * Forces the selection of {@link #trackIndices} for a {@link TrackGroup}. * - *

If multiple {link #tracks} are overridden, as many as possible will be selected depending on - * the player capabilities. + *

If multiple tracks in {@link #trackGroup} are overridden, as many as possible will be + * selected depending on the player capabilities. * - *

If a {@link TrackSelectionOverride} has no tracks ({@code tracks.isEmpty()}), no tracks will - * be played. This is similar to {@link TrackSelectionParameters#disabledTrackTypes}, except it - * will only affect the playback of the associated {@link TrackGroup}. For example, if the only - * {@link C#TRACK_TYPE_VIDEO} {@link TrackGroup} is associated with no tracks, no video will play - * until the next video starts. + *

If {@link #trackIndices} is empty, no tracks from {@link #trackGroup} will be played. This + * is similar to {@link TrackSelectionParameters#disabledTrackTypes}, except it will only affect + * the playback of the associated {@link TrackGroup}. For example, if the only {@link + * C#TRACK_TYPE_VIDEO} {@link TrackGroup} is associated with no tracks, no video will play until + * the next video starts. */ public static final class TrackSelectionOverride implements Bundleable { - /** The {@link TrackGroup} whose {@link #trackIndexes} are forced to be selected. */ + /** The {@link TrackGroup} whose {@link #trackIndices} are forced to be selected. */ public final TrackGroup trackGroup; - /** The index of tracks in a {@link TrackGroup} to be selected. */ - public final ImmutableList trackIndexes; + /** The indices of tracks in a {@link TrackGroup} to be selected. */ + public final ImmutableList trackIndices; /** Constructs an instance to force all tracks in {@code trackGroup} to be selected. */ public TrackSelectionOverride(TrackGroup trackGroup) { @@ -138,23 +138,23 @@ public final class TrackSelectionOverrides implements Bundleable { for (int i = 0; i < trackGroup.length; i++) { builder.add(i); } - this.trackIndexes = builder.build(); + this.trackIndices = builder.build(); } /** - * Constructs an instance to force {@code trackIndexes} in {@code trackGroup} to be selected. + * Constructs an instance to force {@code trackIndices} in {@code trackGroup} to be selected. * * @param trackGroup The {@link TrackGroup} for which to override the track selection. - * @param trackIndexes The indexes of the tracks in the {@link TrackGroup} to select. + * @param trackIndices The indices of the tracks in the {@link TrackGroup} to select. */ - public TrackSelectionOverride(TrackGroup trackGroup, List trackIndexes) { - if (!trackIndexes.isEmpty()) { - if (min(trackIndexes) < 0 || max(trackIndexes) >= trackGroup.length) { + public TrackSelectionOverride(TrackGroup trackGroup, List trackIndices) { + if (!trackIndices.isEmpty()) { + if (min(trackIndices) < 0 || max(trackIndices) >= trackGroup.length) { throw new IndexOutOfBoundsException(); } } this.trackGroup = trackGroup; - this.trackIndexes = ImmutableList.copyOf(trackIndexes); + this.trackIndices = ImmutableList.copyOf(trackIndices); } @Override @@ -166,15 +166,16 @@ public final class TrackSelectionOverrides implements Bundleable { return false; } TrackSelectionOverride that = (TrackSelectionOverride) obj; - return trackGroup.equals(that.trackGroup) && trackIndexes.equals(that.trackIndexes); + return trackGroup.equals(that.trackGroup) && trackIndices.equals(that.trackIndices); } @Override public int hashCode() { - return trackGroup.hashCode() + 31 * trackIndexes.hashCode(); + return trackGroup.hashCode() + 31 * trackIndices.hashCode(); } - private @C.TrackType int getTrackType() { + /** Returns the {@link C.TrackType} of the overriden track group. */ + public @C.TrackType int getTrackType() { return MimeTypes.getTrackType(trackGroup.getFormat(0).sampleMimeType); } @@ -195,7 +196,7 @@ public final class TrackSelectionOverrides implements Bundleable { public Bundle toBundle() { Bundle bundle = new Bundle(); bundle.putBundle(keyForField(FIELD_TRACK_GROUP), trackGroup.toBundle()); - bundle.putIntArray(keyForField(FIELD_TRACKS), Ints.toArray(trackIndexes)); + bundle.putIntArray(keyForField(FIELD_TRACKS), Ints.toArray(trackIndices)); return bundle; } @@ -232,7 +233,7 @@ public final class TrackSelectionOverrides implements Bundleable { return new Builder(overrides); } - /** Returns all {@link TrackSelectionOverride} contained. */ + /** Returns a list of the {@link TrackSelectionOverride overrides}. */ public ImmutableList asList() { return ImmutableList.copyOf(overrides.values()); } 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 a8e2d5aa48..84794d6550 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 @@ -26,7 +26,6 @@ import android.opengl.EGLDisplay; import android.opengl.EGLSurface; import android.opengl.GLES11Ext; import android.opengl.GLES20; -import android.text.TextUtils; import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -87,18 +86,6 @@ public final class GlUtil { this(loadAsset(context, vertexShaderFilePath), loadAsset(context, fragmentShaderFilePath)); } - /** - * Compiles a GL shader program from vertex and fragment shader GLSL GLES20 code. - * - * @param vertexShaderGlsl The vertex shader program as arrays of strings. Strings are joined by - * adding a new line character in between each of them. - * @param fragmentShaderGlsl The fragment shader program as arrays of strings. Strings are - * joined by adding a new line character in between each of them. - */ - public Program(String[] vertexShaderGlsl, String[] fragmentShaderGlsl) { - this(TextUtils.join("\n", vertexShaderGlsl), TextUtils.join("\n", fragmentShaderGlsl)); - } - /** Uses the program. */ public void use() { // Link and check for errors. @@ -119,8 +106,19 @@ public final class GlUtil { GLES20.glDeleteProgram(programId); } + /** + * Returns the location of an {@link Attribute}, which has been enabled as a vertex attribute + * array. + */ + public int getAttributeArrayLocationAndEnable(String attributeName) { + int location = getAttributeLocation(attributeName); + GLES20.glEnableVertexAttribArray(location); + checkGlError(); + return location; + } + /** Returns the location of an {@link Attribute}. */ - public int getAttribLocation(String attributeName) { + private int getAttributeLocation(String attributeName) { return GLES20.glGetAttribLocation(programId, attributeName); } @@ -134,7 +132,7 @@ public final class GlUtil { int[] attributeCount = new int[1]; GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_ATTRIBUTES, attributeCount, 0); if (attributeCount[0] != 2) { - throw new IllegalStateException("Expected two attributes."); + throw new IllegalStateException("Expected two attributes but found " + attributeCount[0]); } Attribute[] attributes = new Attribute[attributeCount[0]]; @@ -169,7 +167,7 @@ public final class GlUtil { GLES20.glGetActiveAttrib( programId, index, length[0], ignore, 0, size, 0, type, 0, nameBytes, 0); String name = new String(nameBytes, 0, strlen(nameBytes)); - int location = getAttribLocation(name); + int location = getAttributeLocation(name); return new Attribute(name, index, location); } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java b/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java index 3cca972822..340f346a3e 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java @@ -268,8 +268,8 @@ public class MediaItemTest { } @Test - @SuppressWarnings("deprecation") // Using deprecated Subtitle type - public void builderSetSubtitles_setsSubtitles() { + @SuppressWarnings("deprecation") // Reading deprecated subtitles field + public void builderSetSubtitleConfigurations() { List subtitleConfigurations = ImmutableList.of( new MediaItem.SubtitleConfiguration.Builder(Uri.parse(URI_STRING + "/es")) @@ -278,7 +278,24 @@ public class MediaItemTest { .setSelectionFlags(C.SELECTION_FLAG_FORCED) .setRoleFlags(C.ROLE_FLAG_ALTERNATE) .setLabel("label") - .build(), + .build()); + + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(URI_STRING) + .setSubtitleConfigurations(subtitleConfigurations) + .build(); + + assertThat(mediaItem.localConfiguration.subtitleConfigurations) + .isEqualTo(subtitleConfigurations); + assertThat(mediaItem.localConfiguration.subtitles).isEqualTo(subtitleConfigurations); + } + + @Test + @SuppressWarnings("deprecation") // Using deprecated Subtitle type + public void builderSetSubtitles() { + List subtitles = + ImmutableList.of( new MediaItem.Subtitle( Uri.parse(URI_STRING + "/en"), MimeTypes.APPLICATION_TTML, /* language= */ "en"), new MediaItem.Subtitle( @@ -295,14 +312,10 @@ public class MediaItemTest { "label")); MediaItem mediaItem = - new MediaItem.Builder() - .setUri(URI_STRING) - .setSubtitleConfigurations(subtitleConfigurations) - .build(); + new MediaItem.Builder().setUri(URI_STRING).setSubtitles(subtitles).build(); - assertThat(mediaItem.localConfiguration.subtitleConfigurations) - .isEqualTo(subtitleConfigurations); - assertThat(mediaItem.localConfiguration.subtitles).isEqualTo(subtitleConfigurations); + assertThat(mediaItem.localConfiguration.subtitleConfigurations).isEqualTo(subtitles); + assertThat(mediaItem.localConfiguration.subtitles).isEqualTo(subtitles); } @Test diff --git a/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java index 4bf3f94bad..fda6c6e888 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java @@ -301,12 +301,13 @@ public class TimelineTest { window.isSeekable = true; window.isDynamic = true; window.liveConfiguration = - new LiveConfiguration( - /* targetOffsetMs= */ 1, - /* minOffsetMs= */ 2, - /* maxOffsetMs= */ 3, - /* minPlaybackSpeed= */ 0.5f, - /* maxPlaybackSpeed= */ 1.5f); + new LiveConfiguration.Builder() + .setTargetOffsetMs(1) + .setMinOffsetMs(2) + .setMaxOffsetMs(3) + .setMinPlaybackSpeed(0.5f) + .setMaxPlaybackSpeed(1.5f) + .build(); window.isPlaceholder = true; window.defaultPositionUs = 444; window.durationUs = 555; diff --git a/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverridesTest.java b/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverridesTest.java index d96e497b1f..5fe642ac48 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverridesTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverridesTest.java @@ -49,7 +49,7 @@ public final class TrackSelectionOverridesTest { new TrackSelectionOverride(newTrackGroupWithIds(1, 2)); assertThat(trackSelectionOverride.trackGroup).isEqualTo(newTrackGroupWithIds(1, 2)); - assertThat(trackSelectionOverride.trackIndexes).containsExactly(0, 1).inOrder(); + assertThat(trackSelectionOverride.trackIndices).containsExactly(0, 1).inOrder(); } @Test @@ -58,7 +58,7 @@ public final class TrackSelectionOverridesTest { new TrackSelectionOverride(newTrackGroupWithIds(1, 2), ImmutableList.of(1)); assertThat(trackSelectionOverride.trackGroup).isEqualTo(newTrackGroupWithIds(1, 2)); - assertThat(trackSelectionOverride.trackIndexes).containsExactly(1); + assertThat(trackSelectionOverride.trackIndices).containsExactly(1); } @Test @@ -67,7 +67,7 @@ public final class TrackSelectionOverridesTest { new TrackSelectionOverride(newTrackGroupWithIds(1, 2), ImmutableList.of()); assertThat(trackSelectionOverride.trackGroup).isEqualTo(newTrackGroupWithIds(1, 2)); - assertThat(trackSelectionOverride.trackIndexes).isEmpty(); + assertThat(trackSelectionOverride.trackIndices).isEmpty(); } @Test @@ -118,9 +118,9 @@ public final class TrackSelectionOverridesTest { public void addOverride_onSameGroup_replacesOverride() { TrackGroup trackGroup = newTrackGroupWithIds(1, 2, 3); TrackSelectionOverride override1 = - new TrackSelectionOverride(trackGroup, /* trackIndexes= */ ImmutableList.of(0)); + new TrackSelectionOverride(trackGroup, /* trackIndices= */ ImmutableList.of(0)); TrackSelectionOverride override2 = - new TrackSelectionOverride(trackGroup, /* trackIndexes= */ ImmutableList.of(1)); + new TrackSelectionOverride(trackGroup, /* trackIndices= */ ImmutableList.of(1)); TrackSelectionOverrides trackSelectionOverrides = new TrackSelectionOverrides.Builder().addOverride(override1).addOverride(override2).build(); diff --git a/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionParametersTest.java b/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionParametersTest.java index f1271ecf7a..519b1d4bed 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionParametersTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionParametersTest.java @@ -77,7 +77,7 @@ public final class TrackSelectionParametersTest { new TrackGroup( new Format.Builder().setId(4).build(), new Format.Builder().setId(5).build()), - /* trackIndexes= */ ImmutableList.of(1))) + /* trackIndices= */ ImmutableList.of(1))) .build(); TrackSelectionParameters parameters = TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ClippedPlaybackTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ClippedPlaybackTest.java index 8cbfd9d552..7d541cd8fa 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ClippedPlaybackTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ClippedPlaybackTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.MediaItem.SubtitleConfiguration; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.ConditionVariable; @@ -44,15 +45,16 @@ public final class ClippedPlaybackTest { MediaItem mediaItem = new MediaItem.Builder() .setUri("asset:///media/mp4/sample.mp4") - .setSubtitles( + .setSubtitleConfigurations( ImmutableList.of( - new MediaItem.Subtitle( - Uri.parse("asset:///media/webvtt/typical"), - MimeTypes.TEXT_VTT, - "en", - C.SELECTION_FLAG_DEFAULT))) + new SubtitleConfiguration.Builder(Uri.parse("asset:///media/webvtt/typical")) + .setMimeType(MimeTypes.TEXT_VTT) + .setLanguage("en") + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build())) // Expect the clipping to affect both subtitles and video. - .setClipEndPositionMs(1000) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder().setEndPositionMs(1000).build()) .build(); AtomicReference player = new AtomicReference<>(); TextCapturingPlaybackListener textCapturer = new TextCapturingPlaybackListener(); @@ -80,21 +82,24 @@ public final class ClippedPlaybackTest { ImmutableList.of( new MediaItem.Builder() .setUri("asset:///media/mp4/sample.mp4") - .setSubtitles( + .setSubtitleConfigurations( ImmutableList.of( - new MediaItem.Subtitle( - Uri.parse("asset:///media/webvtt/typical"), - MimeTypes.TEXT_VTT, - "en", - C.SELECTION_FLAG_DEFAULT))) + new SubtitleConfiguration.Builder( + Uri.parse("asset:///media/webvtt/typical")) + .setMimeType(MimeTypes.TEXT_VTT) + .setLanguage("en") + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build())) // Expect the clipping to affect both subtitles and video. - .setClipEndPositionMs(1000) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder().setEndPositionMs(1000).build()) .build(), new MediaItem.Builder() .setUri("asset:///media/mp4/sample.mp4") // Not needed for correctness, just makes test run faster. Must be longer than the // subtitle content (3.5s). - .setClipEndPositionMs(4_000) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder().setEndPositionMs(4_000).build()) .build()); AtomicReference player = new AtomicReference<>(); TextCapturingPlaybackListener textCapturer = new TextCapturingPlaybackListener(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 91330e7be7..683db1ecbc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer2; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static java.lang.Math.max; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer.InsufficientCapacityException; import com.google.android.exoplayer2.source.SampleStream; @@ -26,6 +28,7 @@ import com.google.android.exoplayer2.source.SampleStream.ReadFlags; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** An abstract base class suitable for most {@link Renderer} implementations. */ public abstract class BaseRenderer implements Renderer, RendererCapabilities { @@ -35,6 +38,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { @Nullable private RendererConfiguration configuration; private int index; + private @MonotonicNonNull PlayerId playerId; private int state; @Nullable private SampleStream stream; @Nullable private Format[] streamFormats; @@ -65,8 +69,9 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { } @Override - public final void setIndex(int index) { + public final void init(int index, PlayerId playerId) { this.index = index; + this.playerId = playerId; } @Override @@ -328,11 +333,24 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return Assertions.checkNotNull(configuration); } - /** Returns the index of the renderer within the player. */ + /** + * Returns the index of the renderer within the player. + * + *

Must only be used after the renderer has been initialized by the player. + */ protected final int getIndex() { return index; } + /** + * Returns the {@link PlayerId} of the player using this renderer. + * + *

Must only be used after the renderer has been initialized by the player. + */ + protected final PlayerId getPlayerId() { + return checkNotNull(playerId); + } + /** * Creates an {@link ExoPlaybackException} of type {@link ExoPlaybackException#TYPE_RENDERER} for * this renderer. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index ca205c2901..97f9ae83c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -191,6 +191,25 @@ public class DefaultRenderersFactory implements RenderersFactory { return this; } + /** + * Enable calling {@link MediaCodec#start} immediately after {@link MediaCodec#flush} on the + * playback thread, when operating the codec in asynchronous mode. If disabled, {@link + * MediaCodec#start} will be called by the callback thread after pending callbacks are handled. + * + *

By default, this feature is disabled. + * + *

This method is experimental, and will be renamed or removed in a future release. + * + * @param enabled Whether {@link MediaCodec#start} will be called on the playback thread + * immediately after {@link MediaCodec#flush}. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory experimentalSetImmediateCodecStartAfterFlushEnabled( + boolean enabled) { + codecAdapterFactory.experimentalSetImmediateCodecStartAfterFlushEnabled(enabled); + return this; + } + /** * Sets whether to enable fallback to lower-priority decoders if decoder initialization fails. * This may result in using a decoder that is less efficient or slower than the primary decoder. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 2143d2a1fb..39db65a02a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -982,6 +982,7 @@ public interface ExoPlayer extends Player { * {@link ExoPlaybackException}. */ @Override + @Nullable ExoPlaybackException getPlayerError(); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 61a1d41b82..f3043f9d1a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -22,6 +22,7 @@ import static java.lang.Math.max; import static java.lang.Math.min; import android.annotation.SuppressLint; +import android.media.metrics.LogSessionId; import android.os.Handler; import android.os.Looper; import android.util.Pair; @@ -30,9 +31,11 @@ import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.ExoPlayer.AudioOffloadListener; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.MediaSource; @@ -246,6 +249,7 @@ import java.util.concurrent.CopyOnWriteArraySet; addListener(analyticsCollector); bandwidthMeter.addEventListener(new Handler(applicationLooper), analyticsCollector); } + PlayerId playerId = Util.SDK_INT < 31 ? new PlayerId() : Api31.createPlayerId(); internalPlayer = new ExoPlayerImplInternal( renderers, @@ -262,7 +266,8 @@ import java.util.concurrent.CopyOnWriteArraySet; pauseAtEndOfMediaItems, applicationLooper, clock, - playbackInfoUpdateListener); + playbackInfoUpdateListener, + playerId); } /** @@ -1856,4 +1861,14 @@ import java.util.concurrent.CopyOnWriteArraySet; return timeline; } } + + @RequiresApi(31) + private static final class Api31 { + private Api31() {} + + public static PlayerId createPlayerId() { + // TODO: Create a MediaMetricsListener and obtain LogSessionId from it. + return new PlayerId(LogSessionId.LOG_SESSION_ID_NONE); + } + } } 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 d6f1e1f73a..ca1ae359f5 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 @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.Player.PlayWhenReadyChangeReason; import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.BehindLiveWindowException; @@ -229,7 +230,8 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean pauseAtEndOfWindow, Looper applicationLooper, Clock clock, - PlaybackInfoUpdateListener playbackInfoUpdateListener) { + PlaybackInfoUpdateListener playbackInfoUpdateListener, + PlayerId playerId) { this.playbackInfoUpdateListener = playbackInfoUpdateListener; this.renderers = renderers; this.trackSelector = trackSelector; @@ -252,7 +254,7 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo); rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { - renderers[i].setIndex(i); + renderers[i].init(/* index= */ i, playerId); rendererCapabilities[i] = renderers[i].getCapabilities(); } mediaClock = new DefaultMediaClock(this, clock); @@ -266,7 +268,8 @@ import java.util.concurrent.atomic.AtomicBoolean; Handler eventHandler = new Handler(applicationLooper); queue = new MediaPeriodQueue(analyticsCollector, eventHandler); - mediaSourceList = new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler); + mediaSourceList = + new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId); // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can // not normally change to this priority" is incorrect. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java index 6a7d298955..95da840f95 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java @@ -21,6 +21,7 @@ import static java.lang.Math.min; import android.os.Handler; import androidx.annotation.Nullable; import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.source.LoadEventInfo; @@ -70,6 +71,7 @@ import java.util.Set; private static final String TAG = "MediaSourceList"; + private final PlayerId playerId; private final List mediaSourceHolders; private final IdentityHashMap mediaSourceByMediaPeriod; private final Map mediaSourceByUid; @@ -93,11 +95,14 @@ import java.util.Set; * source events. * @param analyticsCollectorHandler The {@link Handler} to call {@link AnalyticsCollector} methods * on. + * @param playerId The {@link PlayerId} of the player using this list. */ public MediaSourceList( MediaSourceListInfoRefreshListener listener, @Nullable AnalyticsCollector analyticsCollector, - Handler analyticsCollectorHandler) { + Handler analyticsCollectorHandler, + PlayerId playerId) { + this.playerId = playerId; mediaSourceListInfoListener = listener; shuffleOrder = new DefaultShuffleOrder(0); mediaSourceByMediaPeriod = new IdentityHashMap<>(); @@ -440,7 +445,7 @@ import java.util.Set; childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener)); mediaSource.addEventListener(Util.createHandlerForCurrentOrMainLooper(), eventListener); mediaSource.addDrmEventListener(Util.createHandlerForCurrentOrMainLooper(), eventListener); - mediaSource.prepareSource(caller, mediaTransferListener); + mediaSource.prepareSource(caller, mediaTransferListener, playerId); } private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java b/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java index 4c48cd3141..a3e8081c14 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java @@ -23,6 +23,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; @@ -140,7 +141,8 @@ public final class MetadataRetriever { case MESSAGE_PREPARE_SOURCE: MediaItem mediaItem = (MediaItem) msg.obj; mediaSource = mediaSourceFactory.createMediaSource(mediaItem); - mediaSource.prepareSource(mediaSourceCaller, /* mediaTransferListener= */ null); + mediaSource.prepareSource( + mediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET); mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); return true; case MESSAGE_CHECK_FOR_FAILURE: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 0038b9ab8f..9be14c71b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; @@ -45,7 +46,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities } @Override - public final void setIndex(int index) { + public final void init(int index, PlayerId playerId) { this.index = index; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index b7b8f6bb3c..efefe4c61c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -20,6 +20,7 @@ import android.view.Surface; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AuxEffectInfo; import com.google.android.exoplayer2.source.SampleStream; @@ -248,11 +249,12 @@ public interface Renderer extends PlayerMessage.Target { RendererCapabilities getCapabilities(); /** - * Sets the index of this renderer within the player. + * Initializes the renderer for playback with a player. * - * @param index The renderer index. + * @param index The renderer index within the player. + * @param playerId The {@link PlayerId} of the player. */ - void setIndex(int index); + void init(int index, PlayerId playerId); /** * If the renderer advances its own playback position then this method returns a corresponding diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlayerId.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlayerId.java new file mode 100644 index 0000000000..0e72e7453a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlayerId.java @@ -0,0 +1,75 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.analytics; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; + +import android.media.metrics.LogSessionId; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.util.Util; + +/** Identifier for a player instance. */ +public final class PlayerId { + + /** + * A player identifier with unset default values that can be used as a placeholder or for testing. + */ + public static final PlayerId UNSET = + Util.SDK_INT < 31 ? new PlayerId() : new PlayerId(LogSessionIdApi31.UNSET); + + @Nullable private final LogSessionIdApi31 logSessionIdApi31; + + /** Creates an instance for API < 31. */ + public PlayerId() { + this(/* logSessionIdApi31= */ (LogSessionIdApi31) null); + checkState(Util.SDK_INT < 31); + } + + /** + * Creates an instance for API ≥ 31. + * + * @param logSessionId The {@link LogSessionId} used for this player. + */ + @RequiresApi(31) + public PlayerId(LogSessionId logSessionId) { + this(new LogSessionIdApi31(logSessionId)); + } + + private PlayerId(@Nullable LogSessionIdApi31 logSessionIdApi31) { + this.logSessionIdApi31 = logSessionIdApi31; + } + + /** Returns the {@link LogSessionId} for this player instance. */ + @RequiresApi(31) + public LogSessionId getLogSessionId() { + return checkNotNull(logSessionIdApi31).logSessionId; + } + + @RequiresApi(31) + private static final class LogSessionIdApi31 { + + public static final LogSessionIdApi31 UNSET = + new LogSessionIdApi31(LogSessionId.LOG_SESSION_ID_NONE); + + public final LogSessionId logSessionId; + + public LogSessionIdApi31(LogSessionId logSessionId) { + this.logSessionId = logSessionId; + } + } +} 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 8302bd2903..c56bf54f8c 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 @@ -50,11 +50,7 @@ import java.nio.ByteBuffer; private final Supplier callbackThreadSupplier; private final Supplier queueingThreadSupplier; private final boolean synchronizeCodecInteractionsWithQueueing; - - /** Creates a factory for codecs handling the specified {@link C.TrackType track type}. */ - public Factory(@C.TrackType int trackType) { - this(trackType, /* synchronizeCodecInteractionsWithQueueing= */ false); - } + private final boolean enableImmediateCodecStartAfterFlush; /** * Creates an factory for {@link AsynchronousMediaCodecAdapter} instances. @@ -66,23 +62,29 @@ import java.nio.ByteBuffer; * interactions will wait until all input buffers pending queueing wil be submitted to the * {@link MediaCodec}. */ - public Factory(@C.TrackType int trackType, boolean synchronizeCodecInteractionsWithQueueing) { + public Factory( + @C.TrackType int trackType, + boolean synchronizeCodecInteractionsWithQueueing, + boolean enableImmediateCodecStartAfterFlush) { this( /* callbackThreadSupplier= */ () -> new HandlerThread(createCallbackThreadLabel(trackType)), /* queueingThreadSupplier= */ () -> new HandlerThread(createQueueingThreadLabel(trackType)), - synchronizeCodecInteractionsWithQueueing); + synchronizeCodecInteractionsWithQueueing, + enableImmediateCodecStartAfterFlush); } @VisibleForTesting /* package */ Factory( Supplier callbackThreadSupplier, Supplier queueingThreadSupplier, - boolean synchronizeCodecInteractionsWithQueueing) { + boolean synchronizeCodecInteractionsWithQueueing, + boolean enableImmediateCodecStartAfterFlush) { this.callbackThreadSupplier = callbackThreadSupplier; this.queueingThreadSupplier = queueingThreadSupplier; this.synchronizeCodecInteractionsWithQueueing = synchronizeCodecInteractionsWithQueueing; + this.enableImmediateCodecStartAfterFlush = enableImmediateCodecStartAfterFlush; } @Override @@ -99,7 +101,8 @@ import java.nio.ByteBuffer; codec, callbackThreadSupplier.get(), queueingThreadSupplier.get(), - synchronizeCodecInteractionsWithQueueing); + synchronizeCodecInteractionsWithQueueing, + enableImmediateCodecStartAfterFlush); TraceUtil.endSection(); codecAdapter.initialize( configuration.mediaFormat, @@ -132,6 +135,7 @@ import java.nio.ByteBuffer; private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback; private final AsynchronousMediaCodecBufferEnqueuer bufferEnqueuer; private final boolean synchronizeCodecInteractionsWithQueueing; + private final boolean enableImmediateCodecStartAfterFlush; private boolean codecReleased; @State private int state; @Nullable private Surface inputSurface; @@ -140,11 +144,13 @@ import java.nio.ByteBuffer; MediaCodec codec, HandlerThread callbackThread, HandlerThread enqueueingThread, - boolean synchronizeCodecInteractionsWithQueueing) { + boolean synchronizeCodecInteractionsWithQueueing, + boolean enableImmediateCodecStartAfterFlush) { this.codec = codec; this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread); this.bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, enqueueingThread); this.synchronizeCodecInteractionsWithQueueing = synchronizeCodecInteractionsWithQueueing; + this.enableImmediateCodecStartAfterFlush = enableImmediateCodecStartAfterFlush; this.state = STATE_CREATED; } @@ -231,13 +237,20 @@ import java.nio.ByteBuffer; @Override public void flush() { // The order of calls is important: - // First, flush the bufferEnqueuer to stop queueing input buffers. - // Second, flush the codec to stop producing available input/output buffers. - // Third, flush the callback after flushing the codec so that in-flight callbacks are discarded. + // 1. Flush the bufferEnqueuer to stop queueing input buffers. + // 2. Flush the codec to stop producing available input/output buffers. + // 3. Flush the callback after flushing the codec so that in-flight callbacks are discarded. bufferEnqueuer.flush(); codec.flush(); - // When flushAsync() is completed, start the codec again. - asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ codec::start); + if (enableImmediateCodecStartAfterFlush) { + // The asynchronous callback will drop pending callbacks but we can start the codec now. + asynchronousMediaCodecCallback.flush(/* codec= */ null); + codec.start(); + } else { + // Let the asynchronous callback start the codec in the callback thread after pending + // callbacks are handled. + asynchronousMediaCodecCallback.flush(codec); + } } @Override @@ -289,6 +302,7 @@ import java.nio.ByteBuffer; @Override public void signalEndOfInputStream() { + maybeBlockOnQueueing(); codec.signalEndOfInputStream(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java index 1605c669e7..3e95c2a500 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.mediacodec; +import static androidx.annotation.VisibleForTesting.NONE; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Util.castNonNull; @@ -147,7 +148,7 @@ class AsynchronousMediaCodecBufferEnqueuer { } } - /** Shut down the instance. Make sure to call this method to release its internal resources. */ + /** Shuts down the instance. Make sure to call this method to release its internal resources. */ public void shutdown() { if (started) { flush(); @@ -173,26 +174,23 @@ class AsynchronousMediaCodecBufferEnqueuer { * blocks until the {@link #handlerThread} is idle. */ private void flushHandlerThread() throws InterruptedException { - Handler handler = castNonNull(this.handler); - handler.removeCallbacksAndMessages(null); + checkNotNull(this.handler).removeCallbacksAndMessages(null); blockUntilHandlerThreadIsIdle(); - // Check if any exceptions happened during the last queueing action. - maybeThrowException(); } private void blockUntilHandlerThreadIsIdle() throws InterruptedException { conditionVariable.close(); - castNonNull(handler).obtainMessage(MSG_OPEN_CV).sendToTarget(); + checkNotNull(handler).obtainMessage(MSG_OPEN_CV).sendToTarget(); conditionVariable.block(); } - // Called from the handler thread - - @VisibleForTesting + @VisibleForTesting(otherwise = NONE) /* package */ void setPendingRuntimeException(RuntimeException exception) { pendingRuntimeException.set(exception); } + // Called from the handler thread + private void doHandleMessage(Message msg) { @Nullable MessageParams params = null; switch (msg.what) { @@ -214,7 +212,8 @@ class AsynchronousMediaCodecBufferEnqueuer { conditionVariable.open(); break; default: - setPendingRuntimeException(new IllegalStateException(String.valueOf(msg.what))); + pendingRuntimeException.compareAndSet( + null, new IllegalStateException(String.valueOf(msg.what))); } if (params != null) { recycleMessageParams(params); @@ -226,7 +225,7 @@ class AsynchronousMediaCodecBufferEnqueuer { try { codec.queueInputBuffer(index, offset, size, presentationTimeUs, flag); } catch (RuntimeException e) { - setPendingRuntimeException(e); + pendingRuntimeException.compareAndSet(null, e); } } @@ -240,7 +239,7 @@ class AsynchronousMediaCodecBufferEnqueuer { codec.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags); } } catch (RuntimeException e) { - setPendingRuntimeException(e); + pendingRuntimeException.compareAndSet(null, e); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java index b6fe773f4f..be44abb290 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java @@ -34,8 +34,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @RequiresApi(23) /* package */ final class AsynchronousMediaCodecCallback extends MediaCodec.Callback { private final Object lock; - private final HandlerThread callbackThread; + private @MonotonicNonNull Handler handler; @GuardedBy("lock") @@ -192,14 +192,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * Initiates a flush asynchronously, which will be completed on the callback thread. When the * flush is complete, it will trigger {@code onFlushCompleted} from the callback thread. * - * @param onFlushCompleted A {@link Runnable} that will be called when flush is completed. {@code - * onFlushCompleted} will be called from the scallback thread, therefore it should execute - * synchronized and thread-safe code. + * @param codec A {@link MediaCodec} to {@link MediaCodec#start start} after all pending callbacks + * are handled, or {@code null} if starting the {@link MediaCodec} is performed elsewhere. */ - public void flushAsync(Runnable onFlushCompleted) { + public void flush(@Nullable MediaCodec codec) { synchronized (lock) { ++pendingFlushCount; - Util.castNonNull(handler).post(() -> this.onFlushCompleted(onFlushCompleted)); + Util.castNonNull(handler).post(() -> this.onFlushCompleted(codec)); } } @@ -239,34 +238,31 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } - private void onFlushCompleted(Runnable onFlushCompleted) { + private void onFlushCompleted(@Nullable MediaCodec codec) { synchronized (lock) { - onFlushCompletedSynchronized(onFlushCompleted); - } - } + if (shutDown) { + return; + } - @GuardedBy("lock") - private void onFlushCompletedSynchronized(Runnable onFlushCompleted) { - if (shutDown) { - return; - } - - --pendingFlushCount; - if (pendingFlushCount > 0) { - // Another flush() has been called. - return; - } else if (pendingFlushCount < 0) { - // This should never happen. - setInternalException(new IllegalStateException()); - return; - } - flushInternal(); - try { - onFlushCompleted.run(); - } catch (IllegalStateException e) { - setInternalException(e); - } catch (Exception e) { - setInternalException(new IllegalStateException(e)); + --pendingFlushCount; + if (pendingFlushCount > 0) { + // Another flush() has been called. + return; + } else if (pendingFlushCount < 0) { + // This should never happen. + setInternalException(new IllegalStateException()); + return; + } + flushInternal(); + if (codec != null) { + try { + codec.start(); + } catch (IllegalStateException e) { + setInternalException(e); + } catch (Exception e) { + setInternalException(new IllegalStateException(e)); + } + } } } @@ -275,10 +271,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private void flushInternal() { if (!formats.isEmpty()) { pendingOutputFormat = formats.getLast(); - } else { - // pendingOutputFormat may already be non-null following a previous flush, and remains set in - // this case. } + // else, pendingOutputFormat may already be non-null following a previous flush, and remains + // set in this case. + availableInputBuffers.clear(); availableOutputBuffers.clear(); bufferInfos.clear(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DefaultMediaCodecAdapterFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DefaultMediaCodecAdapterFactory.java index f371d92598..2f12468f9e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DefaultMediaCodecAdapterFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DefaultMediaCodecAdapterFactory.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.mediacodec; +import android.media.MediaCodec; import androidx.annotation.IntDef; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; @@ -46,6 +47,7 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. @Mode private int asynchronousMode; private boolean enableSynchronizeCodecInteractionsWithQueueing; + private boolean enableImmediateCodecStartAfterFlush; public DefaultMediaCodecAdapterFactory() { asynchronousMode = MODE_DEFAULT; @@ -85,6 +87,22 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. enableSynchronizeCodecInteractionsWithQueueing = enabled; } + /** + * Enable calling {@link MediaCodec#start} immediately after {@link MediaCodec#flush} on the + * playback thread, when operating the codec in asynchronous mode. If disabled, {@link + * MediaCodec#start} will be called by the callback thread after pending callbacks are handled. + * + *

By default, this feature is disabled. + * + *

This method is experimental, and will be renamed or removed in a future release. + * + * @param enabled Whether {@link MediaCodec#start()} will be called on the playback thread + * immediately after {@link MediaCodec#flush}. + */ + public void experimentalSetImmediateCodecStartAfterFlushEnabled(boolean enabled) { + enableImmediateCodecStartAfterFlush = enabled; + } + @Override public MediaCodecAdapter createAdapter(MediaCodecAdapter.Configuration configuration) throws IOException { @@ -97,7 +115,9 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. + Util.getTrackTypeString(trackType)); AsynchronousMediaCodecAdapter.Factory factory = new AsynchronousMediaCodecAdapter.Factory( - trackType, enableSynchronizeCodecInteractionsWithQueueing); + trackType, + enableSynchronizeCodecInteractionsWithQueueing, + enableImmediateCodecStartAfterFlush); return factory.createAdapter(configuration); } return new SynchronousMediaCodecAdapter.Factory().createAdapter(configuration); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index e9d6c5829a..575b71fa2f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.ExtractorsFactory; @@ -956,7 +957,8 @@ public final class DownloadHelper { public boolean handleMessage(Message msg) { switch (msg.what) { case MESSAGE_PREPARE_SOURCE: - mediaSource.prepareSource(/* caller= */ this, /* mediaTransferListener= */ null); + mediaSource.prepareSource( + /* caller= */ this, /* mediaTransferListener= */ null, PlayerId.UNSET); mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); return true; case MESSAGE_CHECK_FOR_FAILURE: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java index 8b4cddd8f7..91b0aff28d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java @@ -15,10 +15,13 @@ */ package com.google.android.exoplayer2.source; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; + import android.os.Handler; import android.os.Looper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; @@ -41,6 +44,7 @@ public abstract class BaseMediaSource implements MediaSource { @Nullable private Looper looper; @Nullable private Timeline timeline; + @Nullable private PlayerId playerId; public BaseMediaSource() { mediaSourceCallers = new ArrayList<>(/* initialCapacity= */ 1); @@ -51,7 +55,7 @@ public abstract class BaseMediaSource implements MediaSource { /** * Starts source preparation and enables the source, see {@link #prepareSource(MediaSourceCaller, - * TransferListener)}. This method is called at most once until the next call to {@link + * TransferListener, PlayerId)}. This method is called at most once until the next call to {@link * #releaseSourceInternal()}. * * @param mediaTransferListener The transfer listener which should be informed of any media data @@ -160,6 +164,16 @@ public abstract class BaseMediaSource implements MediaSource { return !enabledMediaSourceCallers.isEmpty(); } + /** + * Returns the {@link PlayerId} of the player using this media source. + * + *

Must only be used when the media source is {@link #prepareSourceInternal(TransferListener) + * prepared}. + */ + protected final PlayerId getPlayerId() { + return checkStateNotNull(playerId); + } + @Override public final void addEventListener(Handler handler, MediaSourceEventListener eventListener) { Assertions.checkNotNull(handler); @@ -186,9 +200,12 @@ public abstract class BaseMediaSource implements MediaSource { @Override public final void prepareSource( - MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { + MediaSourceCaller caller, + @Nullable TransferListener mediaTransferListener, + PlayerId playerId) { Looper looper = Looper.myLooper(); Assertions.checkArgument(this.looper == null || this.looper == looper); + this.playerId = playerId; @Nullable Timeline timeline = this.timeline; mediaSourceCallers.add(caller); if (this.looper == null) { @@ -226,6 +243,7 @@ public abstract class BaseMediaSource implements MediaSource { if (mediaSourceCallers.isEmpty()) { looper = null; timeline = null; + playerId = null; enabledMediaSourceCallers.clear(); releaseSourceInternal(); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index a19504ed7d..b38a0832d4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -117,7 +117,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { childSources.put(id, new MediaSourceAndListener<>(mediaSource, caller, eventListener)); mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener); mediaSource.addDrmEventListener(Assertions.checkNotNull(eventHandler), eventListener); - mediaSource.prepareSource(caller, mediaTransferListener); + mediaSource.prepareSource(caller, mediaTransferListener, getPlayerId()); if (!isEnabled()) { mediaSource.disable(caller); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java index 0cbae12170..e24598032d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java @@ -294,6 +294,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { return this; } + @Deprecated @Override public DefaultMediaSourceFactory setDrmHttpDataSourceFactory( @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { @@ -301,12 +302,14 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { return this; } + @Deprecated @Override public DefaultMediaSourceFactory setDrmUserAgent(@Nullable String userAgent) { delegateFactoryLoader.setDrmUserAgent(userAgent); return this; } + @Deprecated @Override public DefaultMediaSourceFactory setDrmSessionManager( @Nullable DrmSessionManager drmSessionManager) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index fe039f9d16..fc9f38a530 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -20,6 +20,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; @@ -34,7 +35,7 @@ import java.io.IOException; * provide a new timeline whenever the structure of the media changes. The MediaSource * provides these timelines by calling {@link MediaSourceCaller#onSourceInfoRefreshed} on the * {@link MediaSourceCaller}s passed to {@link #prepareSource(MediaSourceCaller, - * TransferListener)}. + * TransferListener, PlayerId)}. *

  • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a * way for the player to load and read the media. @@ -183,6 +184,16 @@ public interface MediaSource { /** Returns the {@link MediaItem} whose media is provided by the source. */ MediaItem getMediaItem(); + /** + * @deprecated Implement {@link #prepareSource(MediaSourceCaller, TransferListener, PlayerId)} + * instead. + */ + @Deprecated + default void prepareSource( + MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { + prepareSource(caller, mediaTransferListener, PlayerId.UNSET); + } + /** * Registers a {@link MediaSourceCaller}. Starts source preparation if needed and enables the * source for the creation of {@link MediaPeriod MediaPerods}. @@ -200,15 +211,20 @@ public interface MediaSource { * transfers. May be null if no listener is available. Note that this listener should be only * informed of transfers related to the media loads and not of auxiliary loads for manifests * and other data. + * @param playerId The {@link PlayerId} of the player using this media source. */ - void prepareSource(MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener); + void prepareSource( + MediaSourceCaller caller, + @Nullable TransferListener mediaTransferListener, + PlayerId playerId); /** * Throws any pending error encountered while loading or refreshing source information. * *

    Should not be called directly from application code. * - *

    Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}. + *

    Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener, + * PlayerId)}. */ void maybeThrowSourceInfoRefreshError() throws IOException; @@ -217,7 +233,8 @@ public interface MediaSource { * *

    Should not be called directly from application code. * - *

    Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}. + *

    Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener, + * PlayerId)}. * * @param caller The {@link MediaSourceCaller} enabling the source. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index 67d019ccee..1adc8e8648 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -175,6 +175,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource return this; } + @Deprecated + @Override public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { if (drmSessionManager == null) { setDrmSessionManagerProvider(null); @@ -184,6 +186,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmHttpDataSourceFactory( @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { @@ -194,6 +197,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmUserAgent(@Nullable String userAgent) { if (!usingCustomDrmSessionManagerProvider) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdsMediaSource.java index 1a5d94ad1f..3a14510b8b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdsMediaSource.java @@ -168,7 +168,7 @@ public final class ServerSideInsertedAdsMediaSource extends BaseMediaSource } mediaSource.addEventListener(handler, /* eventListener= */ this); mediaSource.addDrmEventListener(handler, /* eventListener= */ this); - mediaSource.prepareSource(/* caller= */ this, mediaTransferListener); + mediaSource.prepareSource(/* caller= */ this, mediaTransferListener, getPlayerId()); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 3e0ed7e887..d02940ff71 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -29,7 +29,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.FormatSupport; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; @@ -61,112 +60,36 @@ import java.util.concurrent.atomic.AtomicReference; import org.checkerframework.checker.nullness.compatqual.NullableType; /** - * A default {@link TrackSelector} suitable for most use cases. Track selections are made according - * to configurable {@link Parameters}, which can be set by calling {@link - * Player#setTrackSelectionParameters}. + * A default {@link TrackSelector} suitable for most use cases. * *

    Modifying parameters

    * - * To modify only some aspects of the parameters currently used by a selector, it's possible to - * obtain a {@link ParametersBuilder} initialized with the current {@link Parameters}. The desired - * modifications can be made on the builder, and the resulting {@link Parameters} can then be built - * and set on the selector. For example the following code modifies the parameters to restrict video - * track selections to SD, and to select a German audio track if there is one: - * - *
    {@code
    - * // Build on the current parameters.
    - * TrackSelectionParameters currentParameters = player.getTrackSelectionParameters();
    - * // Build the resulting parameters.
    - * TrackSelectionParameters newParameters = currentParameters
    - *     .buildUpon()
    - *     .setMaxVideoSizeSd()
    - *     .setPreferredAudioLanguage("deu")
    - *     .build();
    - * // Set the new parameters.
    - * player.setTrackSelectionParameters(newParameters);
    - * }
    - * - * Convenience methods and chaining allow this to be written more concisely as: + * Track selection parameters should be modified by obtaining a {@link + * TrackSelectionParameters.Builder} initialized with the current {@link TrackSelectionParameters} + * from the player. The desired modifications can be made on the builder, and the resulting {@link + * TrackSelectionParameters} can then be built and set on the player: * *
    {@code
      * player.setTrackSelectionParameters(
      *     player.getTrackSelectionParameters()
      *         .buildUpon()
      *         .setMaxVideoSizeSd()
    - *         .setPreferredAudioLanguage("deu")
    + *         .setPreferredAudioLanguage("de")
      *         .build());
    + *
      * }
    * - * Selection {@link Parameters} support many different options, some of which are described below. - * - *

    Selecting specific tracks

    - * - * Track selection overrides can be used to select specific tracks. To specify an override for a - * renderer, it's first necessary to obtain the tracks that have been mapped to it: + * Some specialized parameters are only available in the extended {@link Parameters} class, which + * can be retrieved and modified in a similar way in this track selector: * *
    {@code
    - * MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
    - * TrackGroupArray rendererTrackGroups = mappedTrackInfo == null ? null
    - *     : mappedTrackInfo.getTrackGroups(rendererIndex);
    - * }
    - * - * If {@code rendererTrackGroups} is null then there aren't any currently mapped tracks, and so - * setting an override isn't possible. Note that a {@link Player.Listener} registered on the player - * can be used to determine when the current tracks (and therefore the mapping) changes. If {@code - * rendererTrackGroups} is non-null then an override can be set. The next step is to query the - * properties of the available tracks to determine the {@code groupIndex} and the {@code - * trackIndices} within the group it that should be selected. The override can then be specified - * using {@link ParametersBuilder#setSelectionOverride}: - * - *
    {@code
    - * SelectionOverride selectionOverride = new SelectionOverride(groupIndex, trackIndices);
    - * player.setTrackSelectionParameters(
    - *     ((Parameters)player.getTrackSelectionParameters())
    + * defaultTrackSelector.setParameters(
    + *     defaultTrackSelector.getParameters()
      *         .buildUpon()
    - *         .setSelectionOverride(rendererIndex, rendererTrackGroups, selectionOverride)
    + *         .setTunnelingEnabled(true)
      *         .build());
    + *
      * }
    - * - *

    Constraint based track selection

    - * - * Whilst track selection overrides make it possible to select specific tracks, the recommended way - * of controlling which tracks are selected is by specifying constraints. For example consider the - * case of wanting to restrict video track selections to SD, and preferring German audio tracks. - * Track selection overrides could be used to select specific tracks meeting these criteria, however - * a simpler and more flexible approach is to specify these constraints directly: - * - *
    {@code
    - * player.setTrackSelectionParameters(
    - *     player.getTrackSelectionParameters()
    - *         .buildUpon()
    - *         .setMaxVideoSizeSd()
    - *         .setPreferredAudioLanguage("deu")
    - *         .build());
    - * }
    - * - * There are several benefits to using constraint based track selection instead of specific track - * overrides: - * - * - * - *

    Disabling renderers

    - * - * Renderers can be disabled using {@link ParametersBuilder#setRendererDisabled}. Disabling a - * renderer differs from setting a {@code null} override because the renderer is disabled - * unconditionally, whereas a {@code null} override is applied only when the track groups available - * to the renderer match the {@link TrackGroupArray} for which it was specified. - * - *

    Tunneling

    - * - * Tunneled playback can be enabled in cases where the combination of renderers and selected tracks - * supports it. This can be done by using {@link ParametersBuilder#setTunnelingEnabled(boolean)}. */ public class DefaultTrackSelector extends MappingTrackSelector { @@ -307,7 +230,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { rendererDisabledFlags = makeSparseBooleanArrayFromTrueKeys( bundle.getIntArray( - Parameters.keyForField(Parameters.FIELD_RENDERER_DISABLED_INDEXES))); + Parameters.keyForField(Parameters.FIELD_RENDERER_DISABLED_INDICES))); } @Override @@ -825,9 +748,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { private void setSelectionOverridesFromBundle(Bundle bundle) { @Nullable - int[] rendererIndexes = + int[] rendererIndices = bundle.getIntArray( - Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_RENDERER_INDEXES)); + Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_RENDERER_INDICES)); List trackGroupArrays = BundleableUtil.fromBundleNullableList( TrackGroupArray.CREATOR, @@ -841,11 +764,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES)), /* defaultValue= */ new SparseArray<>()); - if (rendererIndexes == null || rendererIndexes.length != trackGroupArrays.size()) { + if (rendererIndices == null || rendererIndices.length != trackGroupArrays.size()) { return; // Incorrect format, ignore all overrides. } - for (int i = 0; i < rendererIndexes.length; i++) { - int rendererIndex = rendererIndexes[i]; + for (int i = 0; i < rendererIndices.length; i++) { + int rendererIndex = rendererIndices[i]; TrackGroupArray groups = trackGroupArrays.get(i); @Nullable SelectionOverride selectionOverride = selectionOverrides.get(i); setSelectionOverride(rendererIndex, groups, selectionOverride); @@ -1009,7 +932,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param rendererIndex The renderer index. * @param groups The {@link TrackGroupArray}. * @return Whether there is an override. + * @deprecated Only works to retrieve the overrides set with the deprecated {@link + * ParametersBuilder#setSelectionOverride(int, TrackGroupArray, SelectionOverride)}. Use + * {@link TrackSelectionParameters#trackSelectionOverrides} instead. */ + @Deprecated public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { Map overrides = selectionOverrides.get(rendererIndex); @@ -1022,7 +949,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param rendererIndex The renderer index. * @param groups The {@link TrackGroupArray}. * @return The override, or null if no override exists. + * @deprecated Only works to retrieve the overrides set with the deprecated {@link + * ParametersBuilder#setSelectionOverride(int, TrackGroupArray, SelectionOverride)}. Use + * {@link TrackSelectionParameters#trackSelectionOverrides} instead. */ + @Deprecated @Nullable public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { Map overrides = @@ -1107,10 +1038,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY, FIELD_TUNNELING_ENABLED, FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS, - FIELD_SELECTION_OVERRIDES_RENDERER_INDEXES, + FIELD_SELECTION_OVERRIDES_RENDERER_INDICES, FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS, FIELD_SELECTION_OVERRIDES, - FIELD_RENDERER_DISABLED_INDEXES, + FIELD_RENDERER_DISABLED_INDICES, }) private @interface FieldNumber {} @@ -1126,10 +1057,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static final int FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY = 1008; private static final int FIELD_TUNNELING_ENABLED = 1009; private static final int FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS = 1010; - private static final int FIELD_SELECTION_OVERRIDES_RENDERER_INDEXES = 1011; + private static final int FIELD_SELECTION_OVERRIDES_RENDERER_INDICES = 1011; private static final int FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS = 1012; private static final int FIELD_SELECTION_OVERRIDES = 1013; - private static final int FIELD_RENDERER_DISABLED_INDEXES = 1014; + private static final int FIELD_RENDERER_DISABLED_INDICES = 1014; @Override public Bundle toBundle() { @@ -1172,7 +1103,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { putSelectionOverridesToBundle(bundle, selectionOverrides); // Only true values are put into rendererDisabledFlags. bundle.putIntArray( - keyForField(FIELD_RENDERER_DISABLED_INDEXES), + keyForField(FIELD_RENDERER_DISABLED_INDICES), getKeysFromSparseBooleanArray(rendererDisabledFlags)); return bundle; @@ -1194,7 +1125,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static void putSelectionOverridesToBundle( Bundle bundle, SparseArray> selectionOverrides) { - ArrayList rendererIndexes = new ArrayList<>(); + ArrayList rendererIndices = new ArrayList<>(); ArrayList trackGroupArrays = new ArrayList<>(); SparseArray selections = new SparseArray<>(); @@ -1207,10 +1138,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { selections.put(trackGroupArrays.size(), selection); } trackGroupArrays.add(override.getKey()); - rendererIndexes.add(rendererIndex); + rendererIndices.add(rendererIndex); } bundle.putIntArray( - keyForField(FIELD_SELECTION_OVERRIDES_RENDERER_INDEXES), Ints.toArray(rendererIndexes)); + keyForField(FIELD_SELECTION_OVERRIDES_RENDERER_INDICES), Ints.toArray(rendererIndices)); bundle.putParcelableArrayList( keyForField(FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS), BundleableUtil.toBundleArrayList(trackGroupArrays)); @@ -1504,9 +1435,32 @@ public class DefaultTrackSelector extends MappingTrackSelector { rendererMixedMimeTypeAdaptationSupports, params); - // Apply track disabling and overriding. + // Apply per track type overrides. + SparseArray> applicableOverridesByTrackType = + getApplicableOverrides(mappedTrackInfo, params); + for (int i = 0; i < applicableOverridesByTrackType.size(); i++) { + Pair overrideAndRendererIndex = + applicableOverridesByTrackType.valueAt(i); + applyTrackTypeOverride( + mappedTrackInfo, + definitions, + /* trackType= */ applicableOverridesByTrackType.keyAt(i), + /* override= */ overrideAndRendererIndex.first, + /* overrideRendererIndex= */ overrideAndRendererIndex.second); + } + + // Apply legacy per renderer overrides. for (int i = 0; i < rendererCount; i++) { - definitions[i] = maybeApplyOverride(mappedTrackInfo, params, i, definitions[i]); + if (hasLegacyRendererOverride(mappedTrackInfo, params, /* rendererIndex= */ i)) { + definitions[i] = getLegacyRendererOverride(mappedTrackInfo, params, /* rendererIndex= */ i); + } + } + + // Disable renderers if needed. + for (int i = 0; i < rendererCount; i++) { + if (isRendererDisabled(mappedTrackInfo, params, /* rendererIndex= */ i)) { + definitions[i] = null; + } } @NullableType @@ -1538,44 +1492,94 @@ public class DefaultTrackSelector extends MappingTrackSelector { return Pair.create(rendererConfigurations, rendererTrackSelections); } - /** - * Returns the {@link ExoTrackSelection.Definition} of a renderer after applying selection - * overriding and renderer disabling. - */ - protected ExoTrackSelection.@NullableType Definition maybeApplyOverride( - MappedTrackInfo mappedTrackInfo, - Parameters params, - int rendererIndex, - ExoTrackSelection.@NullableType Definition currentDefinition) { - // Per renderer and per track type disabling + private boolean isRendererDisabled( + MappedTrackInfo mappedTrackInfo, Parameters params, int rendererIndex) { @C.TrackType int rendererType = mappedTrackInfo.getRendererType(rendererIndex); - if (params.getRendererDisabled(rendererIndex) - || params.disabledTrackTypes.contains(rendererType)) { + return params.getRendererDisabled(rendererIndex) + || params.disabledTrackTypes.contains(rendererType); + } + + @SuppressWarnings("deprecation") // Calling deprecated hasSelectionOverride. + private boolean hasLegacyRendererOverride( + MappedTrackInfo mappedTrackInfo, Parameters params, int rendererIndex) { + TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + return params.hasSelectionOverride(rendererIndex, rendererTrackGroups); + } + + @SuppressWarnings("deprecation") // Calling deprecated getSelectionOverride. + private ExoTrackSelection.@NullableType Definition getLegacyRendererOverride( + MappedTrackInfo mappedTrackInfo, Parameters params, int rendererIndex) { + TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + @Nullable + SelectionOverride override = params.getSelectionOverride(rendererIndex, rendererTrackGroups); + if (override == null) { return null; } - // Per TrackGroupArray override - TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); - if (params.hasSelectionOverride(rendererIndex, rendererTrackGroups)) { - @Nullable - SelectionOverride override = params.getSelectionOverride(rendererIndex, rendererTrackGroups); - if (override == null) { - return null; - } - return new ExoTrackSelection.Definition( - rendererTrackGroups.get(override.groupIndex), override.tracks, override.type); - } - // Per TrackGroup override - for (int j = 0; j < rendererTrackGroups.length; j++) { - TrackGroup trackGroup = rendererTrackGroups.get(j); - @Nullable - TrackSelectionOverride overrideTracks = - params.trackSelectionOverrides.getOverride(trackGroup); - if (overrideTracks != null) { - return new ExoTrackSelection.Definition( - trackGroup, Ints.toArray(overrideTracks.trackIndexes)); + return new ExoTrackSelection.Definition( + rendererTrackGroups.get(override.groupIndex), override.tracks, override.type); + } + + /** + * Returns applicable overrides. Mapping from track type to a pair of override and renderer index + * for this override. + */ + private SparseArray> getApplicableOverrides( + MappedTrackInfo mappedTrackInfo, Parameters params) { + SparseArray> applicableOverrides = new SparseArray<>(); + // Iterate through all existing track groups to ensure only overrides for those groups are used. + int rendererCount = mappedTrackInfo.getRendererCount(); + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + for (int j = 0; j < rendererTrackGroups.length; j++) { + maybeUpdateApplicableOverrides( + applicableOverrides, + params.trackSelectionOverrides.getOverride(rendererTrackGroups.get(j)), + rendererIndex); + } + } + // Also iterate unmapped groups to see if they have overrides. + TrackGroupArray unmappedGroups = mappedTrackInfo.getUnmappedTrackGroups(); + for (int i = 0; i < unmappedGroups.length; i++) { + maybeUpdateApplicableOverrides( + applicableOverrides, + params.trackSelectionOverrides.getOverride(unmappedGroups.get(i)), + /* rendererIndex= */ C.INDEX_UNSET); + } + return applicableOverrides; + } + + private void maybeUpdateApplicableOverrides( + SparseArray> applicableOverrides, + @Nullable TrackSelectionOverride override, + int rendererIndex) { + if (override == null) { + return; + } + @C.TrackType int trackType = override.getTrackType(); + @Nullable + Pair existingOverride = applicableOverrides.get(trackType); + if (existingOverride == null || existingOverride.first.trackIndices.isEmpty()) { + // We only need to choose one non-empty override per type. + applicableOverrides.put(trackType, Pair.create(override, rendererIndex)); + } + } + + private void applyTrackTypeOverride( + MappedTrackInfo mappedTrackInfo, + ExoTrackSelection.@NullableType Definition[] definitions, + @C.TrackType int trackType, + TrackSelectionOverride override, + int overrideRendererIndex) { + for (int i = 0; i < definitions.length; i++) { + if (overrideRendererIndex == i) { + definitions[i] = + new ExoTrackSelection.Definition( + override.trackGroup, Ints.toArray(override.trackIndices)); + } else if (mappedTrackInfo.getRendererType(i) == trackType) { + // Disable other renderers of the same type. + definitions[i] = null; } } - return currentDefinition; // No override } // Track selection prior to overrides and disabled flags being applied. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index 2db19fb6a7..e606cfe148 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -465,7 +465,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList /** * Returns initial bitrate group assignments for a {@code country}. The initial bitrate is a list - * of indexes for [Wifi, 2G, 3G, 4G, 5G_NSA, 5G_SA]. + * of indices for [Wifi, 2G, 3G, 4G, 5G_NSA, 5G_SA]. */ private static int[] getInitialBitrateCountryGroupAssignment(String country) { switch (country) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java index 1d9dcadbfb..8f61630e19 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java @@ -167,8 +167,7 @@ public final class VideoDecoderGLSurfaceView extends GLSurfaceView public void onSurfaceCreated(GL10 unused, EGLConfig config) { program = new GlUtil.Program(VERTEX_SHADER, FRAGMENT_SHADER); program.use(); - int posLocation = program.getAttribLocation("in_pos"); - GLES20.glEnableVertexAttribArray(posLocation); + int posLocation = program.getAttributeArrayLocationAndEnable("in_pos"); GLES20.glVertexAttribPointer( posLocation, 2, @@ -176,13 +175,9 @@ public final class VideoDecoderGLSurfaceView extends GLSurfaceView /* normalized= */ false, /* stride= */ 0, TEXTURE_VERTICES); - texLocations[0] = program.getAttribLocation("in_tc_y"); - GLES20.glEnableVertexAttribArray(texLocations[0]); - texLocations[1] = program.getAttribLocation("in_tc_u"); - GLES20.glEnableVertexAttribArray(texLocations[1]); - texLocations[2] = program.getAttribLocation("in_tc_v"); - GLES20.glEnableVertexAttribArray(texLocations[2]); - GlUtil.checkGlError(); + texLocations[0] = program.getAttributeArrayLocationAndEnable("in_tc_y"); + texLocations[1] = program.getAttributeArrayLocationAndEnable("in_tc_u"); + texLocations[2] = program.getAttributeArrayLocationAndEnable("in_tc_v"); colorMatrixLocation = program.getUniformLocation("mColorConversion"); GlUtil.checkGlError(); setupTextures(); @@ -255,9 +250,9 @@ public final class VideoDecoderGLSurfaceView extends GLSurfaceView int[] widths = new int[3]; widths[0] = outputBuffer.width; - // TODO: Handle streams where chroma channels are not stored at half width and height - // compared to luma channel. See [Internal: b/142097774]. - // U and V planes are being stored at half width compared to Y. + // TODO(b/142097774): Handle streams where chroma channels are not stored at half width and + // height compared to the luma channel. U and V planes are being stored at half width compared + // to Y. widths[1] = widths[2] = (widths[0] + 1) / 2; for (int i = 0; i < 3; i++) { // Set cropping of stride if either width or stride has changed. 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 c52e5ddd57..5f88b05488 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 @@ -46,33 +46,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } // Basic vertex & fragment shaders to render a mesh with 3D position & 2D texture data. - private static final String[] VERTEX_SHADER_CODE = - new String[] { - "uniform mat4 uMvpMatrix;", - "uniform mat3 uTexMatrix;", - "attribute vec4 aPosition;", - "attribute vec2 aTexCoords;", - "varying vec2 vTexCoords;", - - // Standard transformation. - "void main() {", - " gl_Position = uMvpMatrix * aPosition;", - " vTexCoords = (uTexMatrix * vec3(aTexCoords, 1)).xy;", - "}" - }; - private static final String[] FRAGMENT_SHADER_CODE = - new String[] { - // This is required since the texture data is GL_TEXTURE_EXTERNAL_OES. - "#extension GL_OES_EGL_image_external : require", - "precision mediump float;", - - // Standard texture rendering shader. - "uniform samplerExternalOES uTexture;", - "varying vec2 vTexCoords;", - "void main() {", - " gl_FragColor = texture2D(uTexture, vTexCoords);", - "}" - }; + private static final String VERTEX_SHADER = + "uniform mat4 uMvpMatrix;\n" + + "uniform mat3 uTexMatrix;\n" + + "attribute vec4 aPosition;\n" + + "attribute vec2 aTexCoords;\n" + + "varying vec2 vTexCoords;\n" + + "// Standard transformation.\n" + + "void main() {\n" + + " gl_Position = uMvpMatrix * aPosition;\n" + + " vTexCoords = (uTexMatrix * vec3(aTexCoords, 1)).xy;\n" + + "}\n"; + private static final String FRAGMENT_SHADER = + "// This is required since the texture data is GL_TEXTURE_EXTERNAL_OES.\n" + + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "// Standard texture rendering shader.\n" + + "uniform samplerExternalOES uTexture;\n" + + "varying vec2 vTexCoords;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(uTexture, vTexCoords);\n" + + "}\n"; // Texture transform matrices. private static final float[] TEX_MATRIX_WHOLE = { @@ -121,11 +115,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Initializes of the GL components. */ /* package */ void init() { - program = new GlUtil.Program(VERTEX_SHADER_CODE, FRAGMENT_SHADER_CODE); + program = new GlUtil.Program(VERTEX_SHADER, FRAGMENT_SHADER); mvpMatrixHandle = program.getUniformLocation("uMvpMatrix"); uTexMatrixHandle = program.getUniformLocation("uTexMatrix"); - positionHandle = program.getAttribLocation("aPosition"); - texCoordsHandle = program.getAttribLocation("aTexCoords"); + positionHandle = program.getAttributeArrayLocationAndEnable("aPosition"); + texCoordsHandle = program.getAttributeArrayLocationAndEnable("aTexCoords"); textureHandle = program.getUniformLocation("uTexture"); } @@ -148,10 +142,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; checkNotNull(program).use(); checkGlError(); - GLES20.glEnableVertexAttribArray(positionHandle); - GLES20.glEnableVertexAttribArray(texCoordsHandle); - checkGlError(); - float[] texMatrix; if (stereoMode == C.STEREO_MODE_TOP_BOTTOM) { texMatrix = rightEye ? TEX_MATRIX_BOTTOM : TEX_MATRIX_TOP; @@ -162,6 +152,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } GLES20.glUniformMatrix3fv(uTexMatrixHandle, 1, false, texMatrix, 0); + // TODO(b/205002913): Update to use GlUtil.Uniform.bind(). GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); 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 58f6522b1d..cc05a87558 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 @@ -44,12 +44,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(42_000); } @@ -60,12 +61,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 4321, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(4321) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(400_000); } @@ -76,12 +78,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 3, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(3) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(5_000); } @@ -93,12 +96,13 @@ public class DefaultLivePlaybackSpeedControlTest { defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(321_000); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -113,12 +117,13 @@ public class DefaultLivePlaybackSpeedControlTest { defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(123_456_789); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -133,12 +138,13 @@ public class DefaultLivePlaybackSpeedControlTest { defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(3_141); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -164,12 +170,13 @@ public class DefaultLivePlaybackSpeedControlTest { new DefaultLivePlaybackSpeedControl.Builder().build(); defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(123_456_789); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(C.TIME_UNSET); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -184,12 +191,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetLiveOffsetIncrementOnRebufferMs(3) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); defaultLivePlaybackSpeedControl.notifyRebuffer(); @@ -206,12 +214,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetLiveOffsetIncrementOnRebufferMs(3) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); List targetOffsetsUs = new ArrayList<>(); for (int i = 0; i < 500; i++) { @@ -231,12 +240,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetLiveOffsetIncrementOnRebufferMs(0) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -252,12 +262,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetLiveOffsetIncrementOnRebufferMs(3) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(321_000); @@ -274,22 +285,24 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetLiveOffsetIncrementOnRebufferMs(3) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); defaultLivePlaybackSpeedControl.notifyRebuffer(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 3, - /* maxLiveOffsetMs= */ 450, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(3) + .setMaxOffsetMs(450) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); long targetLiveOffsetAfterUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); assertThat(targetLiveOffsetAfterUs).isGreaterThan(targetLiveOffsetBeforeUs); @@ -304,21 +317,23 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetLiveOffsetIncrementOnRebufferMs(3) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 39, - /* minLiveOffsetMs= */ 3, - /* maxLiveOffsetMs= */ 450, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(39) + .setMinOffsetMs(3) + .setMaxOffsetMs(450) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); assertThat(targetLiveOffsetUs).isEqualTo(39_000); @@ -333,12 +348,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setMinUpdateIntervalMs(100) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42_000, - /* minLiveOffsetMs= */ 5_000, - /* maxLiveOffsetMs= */ 400_000, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42_000) + .setMinOffsetMs(5_000) + .setMaxOffsetMs(400_000) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); long targetLiveOffsetAfterRebufferUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -371,12 +387,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setMinUpdateIntervalMs(100) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42_000, - /* minLiveOffsetMs= */ 5_000, - /* maxLiveOffsetMs= */ 400_000, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42_000) + .setMinOffsetMs(5_000) + .setMaxOffsetMs(400_000) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); long targetLiveOffsetAfterRebufferUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -408,12 +425,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42_000, - /* minLiveOffsetMs= */ 5_000, - /* maxLiveOffsetMs= */ 400_000, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42_000) + .setMinOffsetMs(5_000) + .setMaxOffsetMs(400_000) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); // Pretend to have a buffered duration at around the target duration with some artificial noise. @@ -440,12 +458,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setMinPossibleLiveOffsetSmoothingFactor(0f) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42_000, - /* minLiveOffsetMs= */ 5_000, - /* maxLiveOffsetMs= */ 400_000, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42_000) + .setMinOffsetMs(5_000) + .setMaxOffsetMs(400_000) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); // Pretend to have a buffered duration at around the target duration with some artificial noise. @@ -474,12 +493,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setMinUpdateIntervalMs(100) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42_000, - /* minLiveOffsetMs= */ 5_000, - /* maxLiveOffsetMs= */ 400_000, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42_000) + .setMinOffsetMs(5_000) + .setMaxOffsetMs(400_000) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -495,12 +515,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -516,12 +537,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setMaxLiveOffsetErrorMsForUnitSpeed(5) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeedJustAboveLowerErrorMargin = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -539,12 +561,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setProportionalControlFactor(0.01f).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -561,12 +584,13 @@ public class DefaultLivePlaybackSpeedControlTest { new DefaultLivePlaybackSpeedControl.Builder().setProportionalControlFactor(0.01f).build(); defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(2_000_000); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -583,12 +607,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setFallbackMaxPlaybackSpeed(1.5f).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -603,12 +628,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setFallbackMinPlaybackSpeed(0.5f).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -623,12 +649,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setFallbackMaxPlaybackSpeed(1.5f).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ 2f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(2f) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -643,12 +670,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setFallbackMinPlaybackSpeed(0.5f).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ 0.2f, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(0.2f) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -662,12 +690,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed1 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -691,23 +720,25 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed1 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( /* liveOffsetUs= */ 1_500_000, /* bufferedDurationUs= */ 1_000_000); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed2 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( /* liveOffsetUs= */ 2_500_000, /* bufferedDurationUs= */ 1_000_000); @@ -721,23 +752,25 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed1 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( /* liveOffsetUs= */ 1_500_000, /* bufferedDurationUs= */ 1_000_000); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 1_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(1_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed2 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( /* liveOffsetUs= */ 2_500_000, /* bufferedDurationUs= */ 1_000_000); @@ -751,12 +784,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed1 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -774,12 +808,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed1 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index 53bc87e5e6..00e99f8b3a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -23,7 +23,9 @@ import android.net.Uri; import android.os.Handler; import android.os.Looper; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; @@ -78,7 +80,8 @@ public final class MediaPeriodQueueTest { new MediaSourceList( mock(MediaSourceList.MediaSourceListInfoRefreshListener.class), /* analyticsCollector= */ null, - new Handler(Looper.getMainLooper())); + new Handler(Looper.getMainLooper()), + PlayerId.UNSET); rendererCapabilities = new RendererCapabilities[0]; trackSelector = mock(TrackSelector.class); allocator = mock(Allocator.class); @@ -738,7 +741,8 @@ public final class MediaPeriodQueueTest { new MediaSourceList.MediaSourceHolder(fakeMediaSource, /* useLazyPreparation= */ false); mediaSourceList.setMediaSources( ImmutableList.of(mediaSourceHolder), new FakeShuffleOrder(/* length= */ 1)); - mediaSourceHolder.mediaSource.prepareSourceInternal(/* mediaTransferListener */ null); + mediaSourceHolder.mediaSource.prepareSource( + mock(MediaSourceCaller.class), /* mediaTransferListener */ null, PlayerId.UNSET); Timeline playlistTimeline = mediaSourceList.createTimeline(); firstPeriodUid = playlistTimeline.getUidOfPeriod(/* periodIndex= */ 0); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java index ea40519a3c..209fb83547 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.testutil.FakeMediaSource; @@ -54,7 +55,8 @@ public class MediaSourceListTest { new MediaSourceList( mock(MediaSourceList.MediaSourceListInfoRefreshListener.class), /* analyticsCollector= */ null, - Util.createHandlerForCurrentOrMainLooper()); + Util.createHandlerForCurrentOrMainLooper(), + PlayerId.UNSET); } @Test @@ -92,30 +94,30 @@ public class MediaSourceListTest { // Verify prepare is called once on prepare. verify(mockMediaSource1, times(0)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); verify(mockMediaSource2, times(0)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); mediaSourceList.prepare(/* mediaTransferListener= */ null); assertThat(mediaSourceList.isPrepared()).isTrue(); // Verify prepare is called once on prepare. verify(mockMediaSource1, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); verify(mockMediaSource2, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); mediaSourceList.release(); mediaSourceList.prepare(/* mediaTransferListener= */ null); // Verify prepare is called a second time on re-prepare. verify(mockMediaSource1, times(2)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); verify(mockMediaSource2, times(2)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); } @Test @@ -182,10 +184,10 @@ public class MediaSourceListTest { // Verify sources are prepared. verify(mockMediaSource1, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); verify(mockMediaSource2, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); // Set media items again. The second holder is re-used. MediaSource mockMediaSource3 = mock(MediaSource.class); @@ -203,7 +205,7 @@ public class MediaSourceListTest { assertThat(mediaSources.get(1).isRemoved).isFalse(); verify(mockMediaSource2, times(2)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); } @Test @@ -222,10 +224,10 @@ public class MediaSourceListTest { // Verify lazy initialization does not call prepare on sources. verify(mockMediaSource1, times(0)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); verify(mockMediaSource2, times(0)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); for (int i = 0; i < mediaSources.size(); i++) { assertThat(mediaSources.get(i).firstWindowIndexInChild).isEqualTo(i); @@ -259,10 +261,10 @@ public class MediaSourceListTest { // Verify prepare is called on sources when added. verify(mockMediaSource1, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); verify(mockMediaSource2, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); } @Test @@ -387,7 +389,7 @@ public class MediaSourceListTest { new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); verify(mockMediaSource, times(0)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); mediaSourceList.release(); verify(mockMediaSource, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); assertThat(mediaSourceHolder.isRemoved).isFalse(); @@ -406,7 +408,7 @@ public class MediaSourceListTest { new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); verify(mockMediaSource, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); mediaSourceList.release(); verify(mockMediaSource, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); assertThat(mediaSourceHolder.isRemoved).isFalse(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java index 9aa2b5f3ba..3aacf01791 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java @@ -54,7 +54,8 @@ public class AsynchronousMediaCodecAdapterTest { new AsynchronousMediaCodecAdapter.Factory( /* callbackThreadSupplier= */ () -> callbackThread, /* queueingThreadSupplier= */ () -> queueingThread, - /* synchronizeCodecInteractionsWithQueueing= */ false) + /* synchronizeCodecInteractionsWithQueueing= */ false, + /* enableImmediateCodecStartAfterFlush= */ false) .createAdapter(configuration); bufferInfo = new MediaCodec.BufferInfo(); // After starting the MediaCodec, the ShadowMediaCodec offers input buffer 0. We advance the diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java index f3a08df819..4575ad1132 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java @@ -190,6 +190,25 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { enqueuer.flush(); } + @Test + public void flush_withPendingError_doesNotResetError() { + enqueuer.start(); + enqueuer.setPendingRuntimeException( + new MediaCodec.CryptoException(/* errorCode= */ 0, /* detailMessage= */ null)); + + enqueuer.flush(); + + assertThrows( + MediaCodec.CryptoException.class, + () -> + enqueuer.queueInputBuffer( + /* index= */ 0, + /* offset= */ 0, + /* size= */ 0, + /* presentationTimeUs= */ 0, + /* flags= */ 0)); + } + @Test public void shutdown_withoutStart_works() { enqueuer.shutdown(); @@ -219,6 +238,16 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { assertThrows(IllegalStateException.class, () -> enqueuer.shutdown()); } + @Test + public void shutdown_withPendingError_doesNotThrow() { + enqueuer.start(); + enqueuer.setPendingRuntimeException( + new MediaCodec.CryptoException(/* errorCode= */ 0, /* detailMessage= */ null)); + + // Shutting down with a pending error set should not throw . + enqueuer.shutdown(); + } + private static CryptoInfo createCryptoInfo() { CryptoInfo info = new CryptoInfo(); int numSubSamples = 5; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java index fbb031a64b..39d01844d2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java @@ -24,6 +24,7 @@ import static org.robolectric.Shadows.shadowOf; import android.media.MediaCodec; import android.media.MediaFormat; +import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -81,16 +82,24 @@ public class AsynchronousMediaCodecCallbackTest { @Test public void dequeInputBufferIndex_withPendingFlush_returnsTryAgain() { - Looper callbackThreadLooper = callbackThread.getLooper(); + AtomicBoolean beforeFlushCompletes = new AtomicBoolean(); AtomicBoolean flushCompleted = new AtomicBoolean(); + Looper callbackThreadLooper = callbackThread.getLooper(); + Handler callbackHandler = new Handler(callbackThreadLooper); + ShadowLooper shadowCallbackLooper = shadowOf(callbackThreadLooper); // Pause the callback thread so that flush() never completes. - shadowOf(callbackThreadLooper).pause(); + shadowCallbackLooper.pause(); // Send two input buffers to the callback and then flush(). asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0); asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + callbackHandler.post(() -> beforeFlushCompletes.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + callbackHandler.post(() -> flushCompleted.set(true)); + while (!beforeFlushCompletes.get()) { + shadowCallbackLooper.runOneTask(); + } + assertThat(flushCompleted.get()).isFalse(); assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()) .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); @@ -104,8 +113,8 @@ public class AsynchronousMediaCodecCallbackTest { // Send two input buffers to the callback and then flush(). asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0); asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback thread so that flush() completes. shadowOf(callbackThreadLooper).idle(); @@ -123,10 +132,11 @@ public class AsynchronousMediaCodecCallbackTest { // another input buffer. asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0); asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); - // Progress the callback thread so that flush() completes. - shadowOf(callbackThreadLooper).idle(); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); + // Progress the callback thread to complete flush. + shadowOf(callbackThread.getLooper()).idle(); + // Send another input buffer to the callback asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 2); assertThat(flushCompleted.get()).isTrue(); @@ -152,20 +162,6 @@ public class AsynchronousMediaCodecCallbackTest { () -> asynchronousMediaCodecCallback.dequeueInputBufferIndex()); } - @Test - public void dequeueInputBufferIndex_afterFlushCompletedWithError_throwsError() throws Exception { - MediaCodec.CodecException codecException = createCodecException(); - asynchronousMediaCodecCallback.flushAsync( - () -> { - throw codecException; - }); - shadowOf(callbackThread.getLooper()).idle(); - - assertThrows( - MediaCodec.CodecException.class, - () -> asynchronousMediaCodecCallback.dequeueInputBufferIndex()); - } - @Test public void dequeOutputBufferIndex_afterCreation_returnsTryAgain() { MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); @@ -198,17 +194,24 @@ public class AsynchronousMediaCodecCallbackTest { @Test public void dequeOutputBufferIndex_withPendingFlush_returnsTryAgain() { - Looper callbackThreadLooper = callbackThread.getLooper(); + AtomicBoolean beforeFlushCompletes = new AtomicBoolean(); AtomicBoolean flushCompleted = new AtomicBoolean(); + Looper callbackThreadLooper = callbackThread.getLooper(); + Handler callbackHandler = new Handler(callbackThreadLooper); + ShadowLooper shadowCallbackLooper = shadowOf(callbackThreadLooper); // Pause the callback thread so that flush() never completes. - shadowOf(callbackThreadLooper).pause(); + shadowCallbackLooper.pause(); // Send two output buffers to the callback and then flush(). MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + callbackHandler.post(() -> beforeFlushCompletes.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + callbackHandler.post(() -> flushCompleted.set(true)); + while (beforeFlushCompletes.get()) { + shadowCallbackLooper.runOneTask(); + } assertThat(flushCompleted.get()).isFalse(); assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo())) @@ -224,8 +227,8 @@ public class AsynchronousMediaCodecCallbackTest { MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); @@ -245,10 +248,11 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onOutputFormatChanged(codec, createMediaFormat("format0")); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); + // Emulate an output buffer is available. asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 2, bufferInfo); MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); @@ -271,8 +275,8 @@ public class AsynchronousMediaCodecCallbackTest { MediaFormat pendingMediaFormat = new MediaFormat(); asynchronousMediaCodecCallback.onOutputFormatChanged(codec, pendingMediaFormat); // flush() should not discard the last format. - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); // Right after flush(), we send an output buffer: the pending output format should be @@ -298,8 +302,8 @@ public class AsynchronousMediaCodecCallbackTest { MediaFormat pendingMediaFormat = new MediaFormat(); asynchronousMediaCodecCallback.onOutputFormatChanged(codec, pendingMediaFormat); // flush() should not discard the last format. - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); // The first callback after flush() is a new MediaFormat, it should overwrite the pending @@ -335,20 +339,6 @@ public class AsynchronousMediaCodecCallbackTest { () -> asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo())); } - @Test - public void dequeueOutputBufferIndex_afterFlushCompletedWithError_throwsError() throws Exception { - MediaCodec.CodecException codecException = createCodecException(); - asynchronousMediaCodecCallback.flushAsync( - () -> { - throw codecException; - }); - shadowOf(callbackThread.getLooper()).idle(); - - assertThrows( - MediaCodec.CodecException.class, - () -> asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo())); - } - @Test public void getOutputFormat_onNewInstance_raisesException() { try { @@ -377,8 +367,8 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format); asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); @@ -390,7 +380,8 @@ public class AsynchronousMediaCodecCallbackTest { public void getOutputFormat_afterFlushWithPendingFormat_returnsPendingFormat() { MediaCodec.BufferInfo outInfo = new MediaCodec.BufferInfo(); AtomicBoolean flushCompleted = new AtomicBoolean(); - ShadowLooper shadowCallbackLooper = shadowOf(callbackThread.getLooper()); + Looper callbackThreadLooper = callbackThread.getLooper(); + ShadowLooper shadowCallbackLooper = shadowOf(callbackThreadLooper); shadowCallbackLooper.pause(); asynchronousMediaCodecCallback.onOutputFormatChanged(codec, createMediaFormat("format0")); @@ -399,8 +390,8 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onOutputFormatChanged(codec, createMediaFormat("format1")); asynchronousMediaCodecCallback.onOutputBufferAvailable( codec, /* index= */ 1, new MediaCodec.BufferInfo()); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the looper so that flush is completed shadowCallbackLooper.idle(); // Enqueue an output buffer to make the pending format available. @@ -419,7 +410,8 @@ public class AsynchronousMediaCodecCallbackTest { public void getOutputFormat_withConsecutiveFlushAndPendingFormatFromFirstFlush_returnsPendingFormat() { MediaCodec.BufferInfo outInfo = new MediaCodec.BufferInfo(); - AtomicInteger flushesCompleted = new AtomicInteger(); + AtomicInteger flushCompleted = new AtomicInteger(); + Handler callbackThreadHandler = new Handler(callbackThread.getLooper()); ShadowLooper shadowCallbackLooper = shadowOf(callbackThread.getLooper()); shadowCallbackLooper.pause(); @@ -427,17 +419,17 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onOutputBufferAvailable( codec, /* index= */ 0, new MediaCodec.BufferInfo()); // Flush and progress the looper so that flush is completed. - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ flushesCompleted::incrementAndGet); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + callbackThreadHandler.post(flushCompleted::incrementAndGet); shadowCallbackLooper.idle(); // Flush again, the pending format from the first flush should remain as pending. - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ flushesCompleted::incrementAndGet); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + callbackThreadHandler.post(flushCompleted::incrementAndGet); shadowCallbackLooper.idle(); asynchronousMediaCodecCallback.onOutputBufferAvailable( codec, /* index= */ 1, new MediaCodec.BufferInfo()); - assertThat(flushesCompleted.get()).isEqualTo(2); + assertThat(flushCompleted.get()).isEqualTo(2); assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outInfo)) .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); assertThat(asynchronousMediaCodecCallback.getOutputFormat().getString("name")) @@ -446,16 +438,21 @@ public class AsynchronousMediaCodecCallbackTest { } @Test - public void flush_withPendingFlush_onlyLastFlushCompletes() { - ShadowLooper callbackLooperShadow = shadowOf(callbackThread.getLooper()); - callbackLooperShadow.pause(); - AtomicInteger flushCompleted = new AtomicInteger(); + public void flush_withPendingError_resetsError() throws Exception { + asynchronousMediaCodecCallback.onError(codec, createCodecException()); + // Calling flush should clear any pending error. + asynchronousMediaCodecCallback.flush(/* codec= */ null); - asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ () -> flushCompleted.set(1)); - asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ () -> flushCompleted.set(2)); - callbackLooperShadow.idle(); + assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } - assertThat(flushCompleted.get()).isEqualTo(2); + @Test + public void shutdown_withPendingError_doesNotThrow() throws Exception { + asynchronousMediaCodecCallback.onError(codec, createCodecException()); + + // Calling shutdown() should not throw. + asynchronousMediaCodecCallback.shutdown(); } /** Reflectively create a {@link MediaCodec.CodecException}. */ diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index f34ab81e0f..b78a7c4e0f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -24,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; @@ -644,7 +645,7 @@ public final class ConcatenatingMediaSourceTest { () -> { MediaSourceCaller caller = mock(MediaSourceCaller.class); mediaSource.addMediaSources(Arrays.asList(createMediaSources(2))); - mediaSource.prepareSource(caller, /* mediaTransferListener= */ null); + mediaSource.prepareSource(caller, /* mediaTransferListener= */ null, PlayerId.UNSET); mediaSource.moveMediaSource( /* currentIndex= */ 0, /* newIndex= */ 1, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java index ed1960b708..e580d757be 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java @@ -93,12 +93,22 @@ public final class DefaultMediaSourceFactoryTest { public void createMediaSource_withSubtitle_isMergingMediaSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); - List subtitles = + List subtitleConfigurations = Arrays.asList( - new MediaItem.Subtitle(Uri.parse(URI_TEXT), MimeTypes.APPLICATION_TTML, "en"), - new MediaItem.Subtitle( - Uri.parse(URI_TEXT), MimeTypes.APPLICATION_TTML, "de", C.SELECTION_FLAG_DEFAULT)); - MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setSubtitles(subtitles).build(); + new MediaItem.SubtitleConfiguration.Builder(Uri.parse(URI_TEXT)) + .setMimeType(MimeTypes.APPLICATION_TTML) + .setLanguage("en") + .build(), + new MediaItem.SubtitleConfiguration.Builder(Uri.parse(URI_TEXT)) + .setMimeType(MimeTypes.APPLICATION_TTML) + .setLanguage("de") + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build()); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(URI_MEDIA) + .setSubtitleConfigurations(subtitleConfigurations) + .build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -110,7 +120,11 @@ public final class DefaultMediaSourceFactoryTest { DefaultMediaSourceFactory defaultMediaSourceFactory = new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = - new MediaItem.Builder().setUri(URI_MEDIA).setClipStartPositionMs(1000L).build(); + new MediaItem.Builder() + .setUri(URI_MEDIA) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder().setStartPositionMs(1000L).build()) + .build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -122,7 +136,11 @@ public final class DefaultMediaSourceFactoryTest { DefaultMediaSourceFactory defaultMediaSourceFactory = new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = - new MediaItem.Builder().setUri(URI_MEDIA).setClipEndPositionMs(1000L).build(); + new MediaItem.Builder() + .setUri(URI_MEDIA) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder().setEndPositionMs(1000L).build()) + .build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -134,7 +152,13 @@ public final class DefaultMediaSourceFactoryTest { DefaultMediaSourceFactory defaultMediaSourceFactory = new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = - new MediaItem.Builder().setUri(URI_MEDIA).setClipRelativeToDefaultPosition(true).build(); + new MediaItem.Builder() + .setUri(URI_MEDIA) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setRelativeToDefaultPosition(true) + .build()) + .build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -148,7 +172,10 @@ public final class DefaultMediaSourceFactoryTest { MediaItem mediaItem = new MediaItem.Builder() .setUri(URI_MEDIA) - .setClipEndPositionMs(C.TIME_END_OF_SOURCE) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setEndPositionMs(C.TIME_END_OF_SOURCE) + .build()) .build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java index 1580a39f17..5e69dfc755 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java @@ -29,6 +29,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; @@ -117,7 +118,8 @@ public final class AdsMediaSourceTest { adMediaSourceFactory, mockAdsLoader, mockAdViewProvider); - adsMediaSource.prepareSource(mockMediaSourceCaller, /* mediaTransferListener= */ null); + adsMediaSource.prepareSource( + mockMediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET); shadowOf(Looper.getMainLooper()).idle(); verify(mockAdsLoader) .start( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdMediaSourceTest.java index 6b1f858f96..3c0dba79b6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdMediaSourceTest.java @@ -40,6 +40,7 @@ import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.robolectric.PlaybackOutput; import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; @@ -104,7 +105,9 @@ public final class ServerSideInsertedAdMediaSourceTest { mediaSource.setAdPlaybackState(adPlaybackState); mediaSource.prepareSource( - (source, timeline) -> timelineReference.set(timeline), /* mediaTransferListener= */ null); + (source, timeline) -> timelineReference.set(timeline), + /* mediaTransferListener= */ null, + PlayerId.UNSET); runMainLooperUntil(() -> timelineReference.get() != null); Timeline timeline = timelineReference.get(); 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 c2eaf5ddaf..5209adb8dc 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.trackselection; import static com.google.android.exoplayer2.C.FORMAT_EXCEEDS_CAPABILITIES; import static com.google.android.exoplayer2.C.FORMAT_HANDLED; import static com.google.android.exoplayer2.C.FORMAT_UNSUPPORTED_SUBTYPE; +import static com.google.android.exoplayer2.C.FORMAT_UNSUPPORTED_TYPE; import static com.google.android.exoplayer2.RendererCapabilities.ADAPTIVE_NOT_SEAMLESS; import static com.google.android.exoplayer2.RendererCapabilities.TUNNELING_NOT_SUPPORTED; import static com.google.android.exoplayer2.RendererConfiguration.DEFAULT; @@ -52,6 +53,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.HashMap; import java.util.Map; @@ -204,6 +206,44 @@ public final class DefaultTrackSelectorTest { .isEqualTo(new RendererConfiguration[] {DEFAULT, DEFAULT}); } + @Test + public void selectTrack_withMixedEmptyAndNonEmptyTrackOverrides_appliesNonEmptyOverride() + throws Exception { + TrackGroup videoGroupHighBitrate = + new TrackGroup(VIDEO_FORMAT.buildUpon().setAverageBitrate(1_000_000).build()); + TrackGroup videoGroupMidBitrate = + new TrackGroup(VIDEO_FORMAT.buildUpon().setAverageBitrate(500_000).build()); + TrackGroup videoGroupLowBitrate = + new TrackGroup(VIDEO_FORMAT.buildUpon().setAverageBitrate(100_000).build()); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackSelectionOverrides( + new TrackSelectionOverrides.Builder() + .addOverride( + new TrackSelectionOverride( + videoGroupHighBitrate, /* trackIndices= */ ImmutableList.of())) + .addOverride( + new TrackSelectionOverride( + videoGroupMidBitrate, /* trackIndices= */ ImmutableList.of(0))) + .addOverride( + new TrackSelectionOverride( + videoGroupLowBitrate, /* trackIndices= */ ImmutableList.of())) + .build())); + + TrackSelectorResult result = + trackSelector.selectTracks( + RENDERER_CAPABILITIES, + new TrackGroupArray(videoGroupHighBitrate, videoGroupMidBitrate, videoGroupLowBitrate), + periodId, + TIMELINE); + + assertThat(result.selections) + .asList() + .containsExactly(new FixedTrackSelection(videoGroupMidBitrate, /* track= */ 0), null) + .inOrder(); + } + /** Tests that an empty override is not applied for a different set of available track groups. */ @Test public void selectTracks_withEmptyTrackOverrideForDifferentTracks_hasNoEffect() @@ -230,6 +270,97 @@ public final class DefaultTrackSelectorTest { .isEqualTo(new RendererConfiguration[] {DEFAULT, DEFAULT}); } + @Test + public void selectTrack_withOverrideForDifferentRenderer_clearsDefaultSelectionOfSameType() + throws Exception { + Format videoFormatH264 = + VIDEO_FORMAT.buildUpon().setId("H264").setSampleMimeType(MimeTypes.VIDEO_H264).build(); + Format videoFormatAv1 = + VIDEO_FORMAT.buildUpon().setId("AV1").setSampleMimeType(MimeTypes.VIDEO_AV1).build(); + TrackGroup videoGroupH264 = new TrackGroup(videoFormatH264); + TrackGroup videoGroupAv1 = new TrackGroup(videoFormatAv1); + Map rendererCapabilitiesMap = + ImmutableMap.of( + videoFormatH264.id, FORMAT_HANDLED, videoFormatAv1.id, FORMAT_UNSUPPORTED_TYPE); + RendererCapabilities rendererCapabilitiesH264 = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_VIDEO, rendererCapabilitiesMap); + rendererCapabilitiesMap = + ImmutableMap.of( + videoFormatH264.id, FORMAT_UNSUPPORTED_TYPE, videoFormatAv1.id, FORMAT_HANDLED); + RendererCapabilities rendererCapabilitiesAv1 = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_VIDEO, rendererCapabilitiesMap); + + // Try to force selection of one TrackGroup in both directions to ensure the default gets + // overridden without having to know what the default is. + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackSelectionOverrides( + new TrackSelectionOverrides.Builder() + .setOverrideForType(new TrackSelectionOverride(videoGroupH264)) + .build())); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {rendererCapabilitiesH264, rendererCapabilitiesAv1}, + new TrackGroupArray(videoGroupH264, videoGroupAv1), + periodId, + TIMELINE); + + assertThat(result.selections) + .asList() + .containsExactly(new FixedTrackSelection(videoGroupH264, /* track= */ 0), null) + .inOrder(); + + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackSelectionOverrides( + new TrackSelectionOverrides.Builder() + .setOverrideForType(new TrackSelectionOverride(videoGroupAv1)) + .build())); + result = + trackSelector.selectTracks( + new RendererCapabilities[] {rendererCapabilitiesH264, rendererCapabilitiesAv1}, + new TrackGroupArray(videoGroupH264, videoGroupAv1), + periodId, + TIMELINE); + + assertThat(result.selections) + .asList() + .containsExactly(null, new FixedTrackSelection(videoGroupAv1, /* track= */ 0)) + .inOrder(); + } + + @Test + public void selectTracks_withOverrideForUnmappedGroup_disablesAllRenderersOfSameType() + throws Exception { + Format audioSupported = AUDIO_FORMAT.buildUpon().setId("supported").build(); + Format audioUnsupported = AUDIO_FORMAT.buildUpon().setId("unsupported").build(); + TrackGroup audioGroupSupported = new TrackGroup(audioSupported); + TrackGroup audioGroupUnsupported = new TrackGroup(audioUnsupported); + Map audioRendererCapabilitiesMap = + ImmutableMap.of( + audioSupported.id, FORMAT_HANDLED, audioUnsupported.id, FORMAT_UNSUPPORTED_TYPE); + RendererCapabilities audioRendererCapabilties = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, audioRendererCapabilitiesMap); + + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackSelectionOverrides( + new TrackSelectionOverrides.Builder() + .setOverrideForType(new TrackSelectionOverride(audioGroupUnsupported)) + .build())); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {VIDEO_CAPABILITIES, audioRendererCapabilties}, + new TrackGroupArray(VIDEO_TRACK_GROUP, audioGroupSupported, audioGroupUnsupported), + periodId, + TIMELINE); + + assertThat(result.selections).asList().containsExactly(VIDEO_TRACK_SELECTION, null).inOrder(); + } + /** Tests that an override is not applied for a different set of available track groups. */ @Test public void selectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { @@ -1896,7 +2027,7 @@ public final class DefaultTrackSelectorTest { .setOverrideForType( new TrackSelectionOverride( new TrackGroup(AUDIO_FORMAT, AUDIO_FORMAT, AUDIO_FORMAT, AUDIO_FORMAT), - /* trackIndexes= */ ImmutableList.of(0, 2, 3))) + /* trackIndices= */ ImmutableList.of(0, 2, 3))) .build()) .setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_AUDIO)) .build(); 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 327cbb308f..5b901bc092 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 @@ -178,6 +178,7 @@ public final class DashMediaSource extends BaseMediaSource { return this; } + @Deprecated @Override public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { if (drmSessionManager == null) { @@ -188,6 +189,7 @@ public final class DashMediaSource extends BaseMediaSource { return this; } + @Deprecated @Override public Factory setDrmHttpDataSourceFactory( @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { @@ -198,6 +200,7 @@ public final class DashMediaSource extends BaseMediaSource { return this; } + @Deprecated @Override public Factory setDrmUserAgent(@Nullable String userAgent) { if (!usingCustomDrmSessionManagerProvider) { @@ -1053,8 +1056,13 @@ public final class DashMediaSource extends BaseMediaSource { maxPlaybackSpeed = manifest.serviceDescription.maxPlaybackSpeed; } liveConfiguration = - new MediaItem.LiveConfiguration( - targetOffsetMs, minLiveOffsetMs, maxLiveOffsetMs, minPlaybackSpeed, maxPlaybackSpeed); + new MediaItem.LiveConfiguration.Builder() + .setTargetOffsetMs(targetOffsetMs) + .setMinOffsetMs(minLiveOffsetMs) + .setMaxOffsetMs(maxLiveOffsetMs) + .setMinPlaybackSpeed(minPlaybackSpeed) + .setMaxPlaybackSpeed(maxPlaybackSpeed) + .build(); } private void scheduleManifestRefresh(long delayUntilNextLoadMs) { 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 004b7e22c7..de6045f8e6 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 @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; @@ -484,7 +485,7 @@ public final class DashMediaSourceTest { countDownLatch.countDown(); } }; - mediaSource.prepareSource(caller, /* mediaTransferListener= */ null); + mediaSource.prepareSource(caller, /* mediaTransferListener= */ null, PlayerId.UNSET); while (!countDownLatch.await(/* timeout= */ 10, MILLISECONDS)) { ShadowLooper.idleMainLooper(); } diff --git a/library/datasource/src/androidTest/AndroidManifest.xml b/library/datasource/src/androidTest/AndroidManifest.xml index 5edae1db96..82fda9bf67 100644 --- a/library/datasource/src/androidTest/AndroidManifest.xml +++ b/library/datasource/src/androidTest/AndroidManifest.xml @@ -18,10 +18,12 @@ xmlns:tools="http://schemas.android.com/tools" package="com.google.android.exoplayer2.upstream.test"> + dataBounds = WavHeaderReader.skipToSampleData(input); dataStartPosition = dataBounds.first.intValue(); - dataEndPosition = dataBounds.second; + long dataSize = dataBounds.second; + if (rf64SampleDataSize != C.LENGTH_UNSET && dataSize == 0xFFFFFFFFL) { + // Following EBU - Tech 3306-2007, the data size indicated in the ds64 chunk should only be + // used if the size of the data chunk is unset. + dataSize = rf64SampleDataSize; + } + dataEndPosition = dataStartPosition + dataSize; + long inputLength = input.getLength(); + if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) { + Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength); + dataEndPosition = inputLength; + } Assertions.checkNotNull(outputWriter).init(dataStartPosition, dataEndPosition); state = STATE_READING_SAMPLE_DATA; } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index 4541a305d6..03cbb9645a 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -32,20 +32,19 @@ import java.io.IOException; private static final String TAG = "WavHeaderReader"; /** - * Returns whether the given {@code input} starts with a RIFF chunk header, followed by a WAVE - * tag. + * Returns whether the given {@code input} starts with a RIFF or RF64 chunk header, followed by a + * WAVE tag. * * @param input The input stream to peek from. The position should point to the start of the * stream. - * @return Whether the given {@code input} starts with a RIFF chunk header, followed by a WAVE - * tag. + * @return Whether the given {@code input} starts with a RIFF or RF64 chunk header, followed by a + * WAVE tag. * @throws IOException If peeking from the input fails. */ public static boolean checkFileType(ExtractorInput input) throws IOException { ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); - // Attempt to read the RIFF chunk. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - if (chunkHeader.id != WavUtil.RIFF_FOURCC) { + if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.RF64_FOURCC) { return false; } @@ -60,25 +59,44 @@ import java.io.IOException; return true; } + /** + * Reads the ds64 chunk defined in EBU - TECH 3306-2007, if present. If there is no such chunk, + * the input's position is left unchanged. + * + * @param input Input stream to read from. The position should point to the byte following the + * WAVE tag. + * @throws IOException If reading from the input fails. + * @return The value of the data size field in the ds64 chunk, or {@link C#LENGTH_UNSET} if there + * is no such chunk. + */ + public static long readRf64SampleDataSize(ExtractorInput input) throws IOException { + ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); + ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); + if (chunkHeader.id != WavUtil.DS64_FOURCC) { + input.resetPeekPosition(); + return C.LENGTH_UNSET; + } + input.advancePeekPosition(8); // RIFF size + scratch.setPosition(0); + input.peekFully(scratch.getData(), 0, 8); + long sampleDataSize = scratch.readLittleEndianLong(); + input.skipFully(ChunkHeader.SIZE_IN_BYTES + (int) chunkHeader.size); + return sampleDataSize; + } + /** * Reads and returns a {@code WavFormat}. * * @param input Input stream to read the WAV format from. The position should point to the byte - * following the WAVE tag. + * following the ds64 chunk if present, or to the byte following the WAVE tag otherwise. * @throws IOException If reading from the input fails. * @return A new {@code WavFormat} read from {@code input}. */ public static WavFormat readFormat(ExtractorInput input) throws IOException { // Allocate a scratch buffer large enough to store the format chunk. ParsableByteArray scratch = new ParsableByteArray(16); - // Skip chunks until we find the format chunk. - ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - while (chunkHeader.id != WavUtil.FMT_FOURCC) { - input.skipFully(ChunkHeader.SIZE_IN_BYTES + (int) chunkHeader.size); - chunkHeader = ChunkHeader.peek(input, scratch); - } - + ChunkHeader chunkHeader = skipToChunk(/* chunkId= */ WavUtil.FMT_FOURCC, input, scratch); Assertions.checkState(chunkHeader.size >= 16); input.peekFully(scratch.getData(), 0, 16); scratch.setPosition(0); @@ -110,12 +128,14 @@ import java.io.IOException; } /** - * Skips to the data in the given WAV input stream, and returns its bounds. After calling, the - * input stream's position will point to the start of sample data in the WAV. If an exception is - * thrown, the input position will be left pointing to a chunk header. + * Skips to the data in the given WAV input stream, and returns its start position and size. After + * calling, the input stream's position will point to the start of sample data in the WAV. If an + * exception is thrown, the input position will be left pointing to a chunk header (that may not + * be the data chunk header). * * @param input The input stream, whose read position must be pointing to a valid chunk header. - * @return The byte positions at which the data starts (inclusive) and ends (exclusive). + * @return The byte positions at which the data starts (inclusive) and the size of the data, in + * bytes. * @throws ParserException If an error occurs parsing chunks. * @throws IOException If reading from the input fails. */ @@ -125,8 +145,31 @@ import java.io.IOException; ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); // Skip all chunks until we find the data header. + ChunkHeader chunkHeader = skipToChunk(/* chunkId= */ WavUtil.DATA_FOURCC, input, scratch); + // Skip past the "data" header. + input.skipFully(ChunkHeader.SIZE_IN_BYTES); + + long dataStartPosition = input.getPosition(); + return Pair.create(dataStartPosition, chunkHeader.size); + } + + /** + * Skips to the chunk header corresponding to the {@code chunkId} provided. After calling, the + * input stream's position will point to the chunk header with provided {@code chunkId} and the + * peek position to the chunk body. If an exception is thrown, the input position will be left + * pointing to a chunk header (that may not be the one corresponding to the {@code chunkId}). + * + * @param chunkId The ID of the chunk to skip to. + * @param input The input stream, whose read position must be pointing to a valid chunk header. + * @param scratch A scratch buffer to read the chunk headers. + * @return The {@link ChunkHeader} corresponding to the {@code chunkId} provided. + * @throws ParserException If an error occurs parsing chunks. + * @throws IOException If reading from the input fails. + */ + private static ChunkHeader skipToChunk( + int chunkId, ExtractorInput input, ParsableByteArray scratch) throws IOException { ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - while (chunkHeader.id != WavUtil.DATA_FOURCC) { + while (chunkHeader.id != chunkId) { Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; if (bytesToSkip > Integer.MAX_VALUE) { @@ -136,17 +179,7 @@ import java.io.IOException; input.skipFully((int) bytesToSkip); chunkHeader = ChunkHeader.peek(input, scratch); } - // Skip past the "data" header. - input.skipFully(ChunkHeader.SIZE_IN_BYTES); - - long dataStartPosition = input.getPosition(); - long dataEndPosition = dataStartPosition + chunkHeader.size; - long inputLength = input.getLength(); - if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) { - Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength); - dataEndPosition = inputLength; - } - return Pair.create(dataStartPosition, dataEndPosition); + return chunkHeader; } private WavHeaderReader() { diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java index 4217a1528a..976ef3e82e 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java @@ -53,4 +53,10 @@ public final class WavExtractorTest { ExtractorAsserts.assertBehavior( WavExtractor::new, "media/wav/sample_ima_adpcm.wav", simulationConfig); } + + @Test + public void sample_rf64() throws Exception { + ExtractorAsserts.assertBehavior( + WavExtractor::new, "media/wav/sample_rf64.wav", simulationConfig); + } } 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 56ebfb5762..687d1bb0dd 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 @@ -297,6 +297,7 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { if (drmSessionManager == null) { @@ -307,6 +308,7 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmHttpDataSourceFactory( @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { @@ -317,6 +319,7 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmUserAgent(@Nullable String userAgent) { if (!usingCustomDrmSessionManagerProvider) { 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 556bb83bb9..6e1e298184 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 @@ -25,6 +25,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; @@ -752,7 +753,7 @@ public class HlsMediaSourceTest { List timelines = new ArrayList<>(); MediaSource.MediaSourceCaller mediaSourceCaller = (source, timeline) -> timelines.add(timeline); - mediaSource.prepareSource(mediaSourceCaller, null); + mediaSource.prepareSource(mediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET); runMainLooperUntil(() -> timelines.size() == 1); mediaSource.onPrimaryPlaylistRefreshed(secondPlaylist); runMainLooperUntil(() -> timelines.size() == 2); @@ -785,7 +786,9 @@ public class HlsMediaSourceTest { throws TimeoutException { AtomicReference receivedTimeline = new AtomicReference<>(); mediaSource.prepareSource( - (source, timeline) -> receivedTimeline.set(timeline), /* mediaTransferListener= */ null); + (source, timeline) -> receivedTimeline.set(timeline), + /* mediaTransferListener= */ null, + PlayerId.UNSET); runMainLooperUntil(() -> receivedTimeline.get() != null); return receivedTimeline.get(); } 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 fd2f3a49fe..5295bbab5b 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 @@ -205,6 +205,7 @@ public final class SsMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { if (drmSessionManager == null) { @@ -215,6 +216,7 @@ public final class SsMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmHttpDataSourceFactory( @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { @@ -225,6 +227,7 @@ public final class SsMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmUserAgent(@Nullable String userAgent) { if (!usingCustomDrmSessionManagerProvider) { 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 new file mode 100644 index 0000000000..a380b8e55b --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java @@ -0,0 +1,384 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.transformer; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; +import static java.lang.Math.min; + +import android.media.MediaCodec.BufferInfo; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackException; +import com.google.android.exoplayer2.audio.AudioProcessor; +import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; +import com.google.android.exoplayer2.audio.SonicAudioProcessor; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** + * Pipeline to decode audio samples, apply transformations on the raw samples, and re-encode them. + */ +@RequiresApi(18) +/* package */ final class AudioSamplePipeline implements SamplePipeline { + + private static final String TAG = "AudioSamplePipeline"; + private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024; + + private final MediaCodecAdapterWrapper decoder; + private final Format decoderInputFormat; + private final DecoderInputBuffer decoderInputBuffer; + + private final SonicAudioProcessor sonicAudioProcessor; + private final SpeedProvider speedProvider; + + private final DecoderInputBuffer encoderInputBuffer; + private final DecoderInputBuffer encoderOutputBuffer; + + private final Transformation transformation; + private final int rendererIndex; + + private @MonotonicNonNull AudioFormat encoderInputAudioFormat; + private @MonotonicNonNull MediaCodecAdapterWrapper encoder; + private long nextEncoderInputBufferTimeUs; + private long encoderBufferDurationRemainder; + + private ByteBuffer sonicOutputBuffer; + private boolean drainingSonicForSpeedChange; + private float currentSpeed; + + public AudioSamplePipeline( + Format decoderInputFormat, Transformation transformation, int rendererIndex) + throws ExoPlaybackException { + this.decoderInputFormat = decoderInputFormat; + this.transformation = transformation; + this.rendererIndex = rendererIndex; + decoderInputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + encoderInputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + encoderOutputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + sonicAudioProcessor = new SonicAudioProcessor(); + sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER; + speedProvider = new SegmentSpeedProvider(decoderInputFormat); + currentSpeed = speedProvider.getSpeed(0); + try { + this.decoder = MediaCodecAdapterWrapper.createForAudioDecoding(decoderInputFormat); + } catch (IOException e) { + // TODO (internal b/184262323): Assign an adequate error code. + throw ExoPlaybackException.createForRenderer( + e, + TAG, + rendererIndex, + decoderInputFormat, + /* rendererFormatSupport= */ C.FORMAT_HANDLED, + /* isRecoverable= */ false, + PlaybackException.ERROR_CODE_UNSPECIFIED); + } + } + + @Override + public void release() { + sonicAudioProcessor.reset(); + decoder.release(); + if (encoder != null) { + encoder.release(); + } + } + + @Override + public boolean processData() throws ExoPlaybackException { + if (!ensureEncoderAndAudioProcessingConfigured()) { + return false; + } + if (sonicAudioProcessor.isActive()) { + return feedEncoderFromSonic() || feedSonicFromDecoder(); + } else { + return feedEncoderFromDecoder(); + } + } + + @Override + @Nullable + public DecoderInputBuffer dequeueInputBuffer() { + return decoder.maybeDequeueInputBuffer(decoderInputBuffer) ? decoderInputBuffer : null; + } + + @Override + public void queueInputBuffer() { + decoder.queueInputBuffer(decoderInputBuffer); + } + + @Override + @Nullable + public Format getOutputFormat() { + return encoder != null ? encoder.getOutputFormat() : null; + } + + @Override + public boolean isEnded() { + return encoder != null && encoder.isEnded(); + } + + @Override + @Nullable + public DecoderInputBuffer getOutputBuffer() { + if (encoder != null) { + encoderOutputBuffer.data = encoder.getOutputBuffer(); + if (encoderOutputBuffer.data != null) { + encoderOutputBuffer.timeUs = checkNotNull(encoder.getOutputBufferInfo()).presentationTimeUs; + return encoderOutputBuffer; + } + } + return null; + } + + @Override + public void releaseOutputBuffer() { + checkStateNotNull(encoder).releaseOutputBuffer(); + } + + /** + * 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. + */ + @RequiresNonNull({"encoderInputAudioFormat", "encoder"}) + private boolean feedEncoderFromDecoder() { + if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) { + return false; + } + + if (decoder.isEnded()) { + queueEndOfStreamToEncoder(); + return false; + } + + @Nullable ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer(); + if (decoderOutputBuffer == null) { + return false; + } + if (isSpeedChanging(checkNotNull(decoder.getOutputBufferInfo()))) { + flushSonicAndSetSpeed(currentSpeed); + return false; + } + feedEncoder(decoderOutputBuffer); + if (!decoderOutputBuffer.hasRemaining()) { + decoder.releaseOutputBuffer(); + } + return true; + } + + /** + * 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. + */ + @RequiresNonNull({"encoderInputAudioFormat", "encoder"}) + private boolean feedEncoderFromSonic() { + if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) { + return false; + } + + if (!sonicOutputBuffer.hasRemaining()) { + sonicOutputBuffer = sonicAudioProcessor.getOutput(); + if (!sonicOutputBuffer.hasRemaining()) { + if (decoder.isEnded() && sonicAudioProcessor.isEnded()) { + queueEndOfStreamToEncoder(); + } + return false; + } + } + + feedEncoder(sonicOutputBuffer); + return true; + } + + /** + * 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() { + if (drainingSonicForSpeedChange) { + if (sonicAudioProcessor.isEnded() && !sonicOutputBuffer.hasRemaining()) { + flushSonicAndSetSpeed(currentSpeed); + drainingSonicForSpeedChange = false; + } + return false; + } + + // Sonic invalidates any previous output buffer when more input is queued, so we don't queue if + // there is output still to be processed. + if (sonicOutputBuffer.hasRemaining()) { + return false; + } + + if (decoder.isEnded()) { + sonicAudioProcessor.queueEndOfStream(); + return false; + } + checkState(!sonicAudioProcessor.isEnded()); + + @Nullable ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer(); + if (decoderOutputBuffer == null) { + return false; + } + if (isSpeedChanging(checkNotNull(decoder.getOutputBufferInfo()))) { + sonicAudioProcessor.queueEndOfStream(); + drainingSonicForSpeedChange = true; + return false; + } + sonicAudioProcessor.queueInput(decoderOutputBuffer); + if (!decoderOutputBuffer.hasRemaining()) { + decoder.releaseOutputBuffer(); + } + return true; + } + + /** + * 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. + */ + @RequiresNonNull({"encoder", "encoderInputAudioFormat"}) + private void feedEncoder(ByteBuffer inputBuffer) { + ByteBuffer encoderInputBufferData = checkNotNull(encoderInputBuffer.data); + int bufferLimit = inputBuffer.limit(); + inputBuffer.limit(min(bufferLimit, inputBuffer.position() + encoderInputBufferData.capacity())); + encoderInputBufferData.put(inputBuffer); + encoderInputBuffer.timeUs = nextEncoderInputBufferTimeUs; + computeNextEncoderInputBufferTimeUs( + /* bytesWritten= */ encoderInputBufferData.position(), + encoderInputAudioFormat.bytesPerFrame, + encoderInputAudioFormat.sampleRate); + encoderInputBuffer.setFlags(0); + encoderInputBuffer.flip(); + inputBuffer.limit(bufferLimit); + encoder.queueInputBuffer(encoderInputBuffer); + } + + @RequiresNonNull("encoder") + private void queueEndOfStreamToEncoder() { + checkState(checkNotNull(encoderInputBuffer.data).position() == 0); + encoderInputBuffer.timeUs = nextEncoderInputBufferTimeUs; + encoderInputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + encoderInputBuffer.flip(); + // Queuing EOS should only occur with an empty buffer. + encoder.queueInputBuffer(encoderInputBuffer); + } + + /** + * Attempts to configure the {@link #encoder} and Sonic (if applicable), if they have not been + * configured yet, and returns whether they have been configured. + */ + @EnsuresNonNullIf( + expression = {"encoder", "encoderInputAudioFormat"}, + result = true) + private boolean ensureEncoderAndAudioProcessingConfigured() throws ExoPlaybackException { + if (encoder != null && encoderInputAudioFormat != null) { + return true; + } + @Nullable Format decoderOutputFormat = decoder.getOutputFormat(); + if (decoderOutputFormat == null) { + return false; + } + AudioFormat outputAudioFormat = + new AudioFormat( + decoderOutputFormat.sampleRate, + decoderOutputFormat.channelCount, + decoderOutputFormat.pcmEncoding); + if (transformation.flattenForSlowMotion) { + try { + outputAudioFormat = sonicAudioProcessor.configure(outputAudioFormat); + flushSonicAndSetSpeed(currentSpeed); + } catch (AudioProcessor.UnhandledAudioFormatException e) { + // TODO(internal b/192864511): Assign an adequate error code. + throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED); + } + } + String audioMimeType = + transformation.audioMimeType == null + ? decoderInputFormat.sampleMimeType + : transformation.audioMimeType; + try { + encoder = + MediaCodecAdapterWrapper.createForAudioEncoding( + new Format.Builder() + .setSampleMimeType(audioMimeType) + .setSampleRate(outputAudioFormat.sampleRate) + .setChannelCount(outputAudioFormat.channelCount) + .setAverageBitrate(DEFAULT_ENCODER_BITRATE) + .build()); + } catch (IOException e) { + // TODO(internal b/192864511): Assign an adequate error code. + throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED); + } + encoderInputAudioFormat = outputAudioFormat; + return true; + } + + private boolean isSpeedChanging(BufferInfo bufferInfo) { + if (!transformation.flattenForSlowMotion) { + return false; + } + float newSpeed = speedProvider.getSpeed(bufferInfo.presentationTimeUs); + boolean speedChanging = newSpeed != currentSpeed; + currentSpeed = newSpeed; + return speedChanging; + } + + private void flushSonicAndSetSpeed(float speed) { + sonicAudioProcessor.setSpeed(speed); + sonicAudioProcessor.setPitch(speed); + sonicAudioProcessor.flush(); + } + + private ExoPlaybackException createRendererException(Throwable cause, int errorCode) { + return ExoPlaybackException.createForRenderer( + cause, + TAG, + rendererIndex, + decoderInputFormat, + /* rendererFormatSupport= */ C.FORMAT_HANDLED, + /* isRecoverable= */ false, + errorCode); + } + + private void computeNextEncoderInputBufferTimeUs( + long bytesWritten, int bytesPerFrame, int sampleRate) { + // The calculation below accounts for remainders and rounding. Without that it corresponds to + // the following: + // bufferDurationUs = numberOfFramesInBuffer * sampleDurationUs + // where numberOfFramesInBuffer = bytesWritten / bytesPerFrame + // and sampleDurationUs = C.MICROS_PER_SECOND / sampleRate + long numerator = bytesWritten * C.MICROS_PER_SECOND + encoderBufferDurationRemainder; + long denominator = (long) bytesPerFrame * sampleRate; + long bufferDurationUs = numerator / denominator; + encoderBufferDurationRemainder = numerator - bufferDurationUs * denominator; + if (encoderBufferDurationRemainder > 0) { // Ceil division result. + bufferDurationUs += 1; + encoderBufferDurationRemainder -= denominator; + } + nextEncoderInputBufferTimeUs += bufferDurationUs; + } +} diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/PassthroughSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/PassthroughSamplePipeline.java new file mode 100644 index 0000000000..8b540ee105 --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/PassthroughSamplePipeline.java @@ -0,0 +1,77 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.transformer; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; + +/** Pipeline that passes through the samples without any re-encoding or transformation. */ +/* package */ final class PassthroughSamplePipeline implements SamplePipeline { + + private final DecoderInputBuffer buffer; + private final Format format; + + private boolean hasPendingBuffer; + + public PassthroughSamplePipeline(Format format) { + this.format = format; + buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); + hasPendingBuffer = false; + } + + @Override + @Nullable + public DecoderInputBuffer dequeueInputBuffer() { + return hasPendingBuffer ? null : buffer; + } + + @Override + public void queueInputBuffer() { + hasPendingBuffer = true; + } + + @Override + public boolean processData() { + return false; + } + + @Override + public Format getOutputFormat() { + return format; + } + + @Override + @Nullable + public DecoderInputBuffer getOutputBuffer() { + return hasPendingBuffer ? buffer : null; + } + + @Override + public void releaseOutputBuffer() { + buffer.clear(); + hasPendingBuffer = false; + } + + @Override + public boolean isEnded() { + return buffer.isEndOfStream(); + } + + @Override + public void release() {} +} 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 new file mode 100644 index 0000000000..6407b4f440 --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SamplePipeline.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.transformer; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; + +/** + * Pipeline for processing {@link DecoderInputBuffer DecoderInputBuffers}. + * + *

    This pipeline can be used to implement transformations of audio or video samples. + */ +/* package */ interface SamplePipeline { + + /** Returns a buffer if the pipeline is ready to accept input, and {@code null} otherwise. */ + @Nullable + DecoderInputBuffer dequeueInputBuffer(); + + /** + * Informs the pipeline that its input buffer contains new input. + * + *

    Should be called after filling the input buffer from {@link #dequeueInputBuffer()} with new + * input. + */ + void queueInputBuffer(); + + /** + * Process the input data and returns whether more data can be processed by calling this method + * again. + */ + boolean processData() throws ExoPlaybackException; + + /** Returns the output format of the pipeline if available, and {@code null} otherwise. */ + @Nullable + Format getOutputFormat(); + + /** Returns an output buffer if the pipeline has produced output, and {@code null} otherwise */ + @Nullable + DecoderInputBuffer getOutputBuffer(); + + /** + * Releases the pipeline's output buffer. + * + *

    Should be called when the output buffer from {@link #getOutputBuffer()} is no longer needed. + */ + void releaseOutputBuffer(); + + /** Returns whether the pipeline has ended. */ + boolean isEnded(); + + /** Releases all resources held by the pipeline. */ + void release(); +} diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java index 7efa0eb780..e5bcb5022c 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java @@ -18,24 +18,15 @@ package com.google.android.exoplayer2.transformer; import static com.google.android.exoplayer2.source.SampleStream.FLAG_REQUIRE_FORMAT; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; -import static java.lang.Math.min; -import android.media.MediaCodec.BufferInfo; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.PlaybackException; -import com.google.android.exoplayer2.audio.AudioProcessor; -import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; -import com.google.android.exoplayer2.audio.SonicAudioProcessor; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; -import java.io.IOException; -import java.nio.ByteBuffer; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -44,37 +35,18 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /* package */ final class TransformerAudioRenderer extends TransformerBaseRenderer { private static final String TAG = "TransformerAudioRenderer"; - private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024; - private static final float SPEED_UNSET = -1f; private final DecoderInputBuffer decoderInputBuffer; - private final DecoderInputBuffer encoderInputBuffer; - private final SonicAudioProcessor sonicAudioProcessor; - @Nullable private MediaCodecAdapterWrapper decoder; - @Nullable private MediaCodecAdapterWrapper encoder; - @Nullable private SpeedProvider speedProvider; - private @MonotonicNonNull Format decoderInputFormat; - private @MonotonicNonNull AudioFormat encoderInputAudioFormat; - - private ByteBuffer sonicOutputBuffer; - private long nextEncoderInputBufferTimeUs; - private float currentSpeed; + private @MonotonicNonNull SamplePipeline samplePipeline; + private boolean muxerWrapperTrackAdded; private boolean muxerWrapperTrackEnded; - private boolean hasEncoderOutputFormat; - private boolean drainingSonicForSpeedChange; public TransformerAudioRenderer( MuxerWrapper muxerWrapper, TransformerMediaClock mediaClock, Transformation transformation) { super(C.TRACK_TYPE_AUDIO, muxerWrapper, mediaClock, transformation); decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); - encoderInputBuffer = - new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); - sonicAudioProcessor = new SonicAudioProcessor(); - sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER; - nextEncoderInputBufferTimeUs = 0; - currentSpeed = SPEED_UNSET; } @Override @@ -89,201 +61,100 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override protected void onReset() { - decoderInputBuffer.clear(); - decoderInputBuffer.data = null; - encoderInputBuffer.clear(); - encoderInputBuffer.data = null; - sonicAudioProcessor.reset(); - if (decoder != null) { - decoder.release(); - decoder = null; + if (samplePipeline != null) { + samplePipeline.release(); } - if (encoder != null) { - encoder.release(); - encoder = null; - } - speedProvider = null; - sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER; - nextEncoderInputBufferTimeUs = 0; - currentSpeed = SPEED_UNSET; + muxerWrapperTrackAdded = false; muxerWrapperTrackEnded = false; - hasEncoderOutputFormat = false; - drainingSonicForSpeedChange = false; } @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (!isRendererStarted || isEnded()) { + if (!isRendererStarted || isEnded() || !ensureRendererConfigured()) { return; } - if (ensureDecoderConfigured()) { - MediaCodecAdapterWrapper decoder = this.decoder; - if (ensureEncoderAndAudioProcessingConfigured()) { - MediaCodecAdapterWrapper encoder = this.encoder; - while (feedMuxerFromEncoder(encoder)) {} - if (sonicAudioProcessor.isActive()) { - while (feedEncoderFromSonic(decoder, encoder)) {} - while (feedSonicFromDecoder(decoder)) {} - } else { - while (feedEncoderFromDecoder(decoder, encoder)) {} - } - } - while (feedDecoderFromInput(decoder)) {} + while (feedMuxerFromPipeline() || samplePipeline.processData() || feedPipelineFromInput()) {} + } + + /** Attempts to read the input format and to initialize the sample pipeline. */ + @EnsuresNonNullIf(expression = "samplePipeline", result = true) + private boolean ensureRendererConfigured() throws ExoPlaybackException { + if (samplePipeline != null) { + return true; } + FormatHolder formatHolder = getFormatHolder(); + @ReadDataResult + int result = readSource(formatHolder, decoderInputBuffer, /* readFlags= */ FLAG_REQUIRE_FORMAT); + if (result != C.RESULT_FORMAT_READ) { + return false; + } + Format decoderInputFormat = checkNotNull(formatHolder.format); + if ((transformation.audioMimeType != null + && !transformation.audioMimeType.equals(decoderInputFormat.sampleMimeType)) + || transformation.flattenForSlowMotion) { + samplePipeline = new AudioSamplePipeline(decoderInputFormat, transformation, getIndex()); + } else { + samplePipeline = new PassthroughSamplePipeline(decoderInputFormat); + } + return true; } /** - * Attempts to write encoder output data to the muxer, and returns whether it may be possible to - * write more data immediately by calling this method again. + * Attempts to write sample pipeline output data to the muxer, and returns whether it may be + * possible to write more data immediately by calling this method again. */ - private boolean feedMuxerFromEncoder(MediaCodecAdapterWrapper encoder) { - if (!hasEncoderOutputFormat) { - @Nullable Format encoderOutputFormat = encoder.getOutputFormat(); - if (encoderOutputFormat == null) { + @RequiresNonNull("samplePipeline") + private boolean feedMuxerFromPipeline() { + if (!muxerWrapperTrackAdded) { + @Nullable Format samplePipelineOutputFormat = samplePipeline.getOutputFormat(); + if (samplePipelineOutputFormat == null) { return false; } - hasEncoderOutputFormat = true; - muxerWrapper.addTrackFormat(encoderOutputFormat); + muxerWrapperTrackAdded = true; + muxerWrapper.addTrackFormat(samplePipelineOutputFormat); } - if (encoder.isEnded()) { + if (samplePipeline.isEnded()) { muxerWrapper.endTrack(getTrackType()); muxerWrapperTrackEnded = true; return false; } - @Nullable ByteBuffer encoderOutputBuffer = encoder.getOutputBuffer(); - if (encoderOutputBuffer == null) { + @Nullable DecoderInputBuffer samplePipelineOutputBuffer = samplePipeline.getOutputBuffer(); + if (samplePipelineOutputBuffer == null) { return false; } - BufferInfo encoderOutputBufferInfo = checkNotNull(encoder.getOutputBufferInfo()); if (!muxerWrapper.writeSample( getTrackType(), - encoderOutputBuffer, + samplePipelineOutputBuffer.data, /* isKeyFrame= */ true, - encoderOutputBufferInfo.presentationTimeUs)) { + samplePipelineOutputBuffer.timeUs)) { return false; } - encoder.releaseOutputBuffer(); + samplePipeline.releaseOutputBuffer(); return true; } /** - * Attempts to pass decoder output data to the encoder, and returns whether it may be possible to + * Attempts to pass input data to the sample pipeline, and returns whether it may be possible to * pass more data immediately by calling this method again. */ - @RequiresNonNull({"encoderInputAudioFormat"}) - private boolean feedEncoderFromDecoder( - MediaCodecAdapterWrapper decoder, MediaCodecAdapterWrapper encoder) { - if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) { + @RequiresNonNull("samplePipeline") + private boolean feedPipelineFromInput() { + @Nullable DecoderInputBuffer samplePipelineInputBuffer = samplePipeline.dequeueInputBuffer(); + if (samplePipelineInputBuffer == null) { return false; } - if (decoder.isEnded()) { - queueEndOfStreamToEncoder(encoder); - return false; - } - - @Nullable ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer(); - if (decoderOutputBuffer == null) { - return false; - } - if (isSpeedChanging(checkNotNull(decoder.getOutputBufferInfo()))) { - flushSonicAndSetSpeed(currentSpeed); - return false; - } - feedEncoder(encoder, decoderOutputBuffer); - if (!decoderOutputBuffer.hasRemaining()) { - decoder.releaseOutputBuffer(); - } - return true; - } - - /** - * 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. - */ - @RequiresNonNull({"encoderInputAudioFormat"}) - private boolean feedEncoderFromSonic( - MediaCodecAdapterWrapper decoder, MediaCodecAdapterWrapper encoder) { - if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) { - return false; - } - - if (!sonicOutputBuffer.hasRemaining()) { - sonicOutputBuffer = sonicAudioProcessor.getOutput(); - if (!sonicOutputBuffer.hasRemaining()) { - if (decoder.isEnded() && sonicAudioProcessor.isEnded()) { - queueEndOfStreamToEncoder(encoder); - } - return false; - } - } - - feedEncoder(encoder, sonicOutputBuffer); - return true; - } - - /** - * 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(MediaCodecAdapterWrapper decoder) { - if (drainingSonicForSpeedChange) { - if (sonicAudioProcessor.isEnded() && !sonicOutputBuffer.hasRemaining()) { - flushSonicAndSetSpeed(currentSpeed); - drainingSonicForSpeedChange = false; - } - return false; - } - - // Sonic invalidates any previous output buffer when more input is queued, so we don't queue if - // there is output still to be processed. - if (sonicOutputBuffer.hasRemaining()) { - return false; - } - - if (decoder.isEnded()) { - sonicAudioProcessor.queueEndOfStream(); - return false; - } - checkState(!sonicAudioProcessor.isEnded()); - - @Nullable ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer(); - if (decoderOutputBuffer == null) { - return false; - } - if (isSpeedChanging(checkNotNull(decoder.getOutputBufferInfo()))) { - sonicAudioProcessor.queueEndOfStream(); - drainingSonicForSpeedChange = true; - return false; - } - sonicAudioProcessor.queueInput(decoderOutputBuffer); - if (!decoderOutputBuffer.hasRemaining()) { - decoder.releaseOutputBuffer(); - } - return true; - } - - /** - * Attempts to pass input data to the decoder, and returns whether it may be possible to pass more - * data immediately by calling this method again. - */ - private boolean feedDecoderFromInput(MediaCodecAdapterWrapper decoder) { - if (!decoder.maybeDequeueInputBuffer(decoderInputBuffer)) { - return false; - } - - decoderInputBuffer.clear(); @ReadDataResult - int result = readSource(getFormatHolder(), decoderInputBuffer, /* readFlags= */ 0); + int result = readSource(getFormatHolder(), samplePipelineInputBuffer, /* readFlags= */ 0); switch (result) { case C.RESULT_BUFFER_READ: - mediaClock.updateTimeForTrackType(getTrackType(), decoderInputBuffer.timeUs); - decoderInputBuffer.timeUs -= streamOffsetUs; - decoderInputBuffer.flip(); - decoder.queueInputBuffer(decoderInputBuffer); - return !decoderInputBuffer.isEndOfStream(); + mediaClock.updateTimeForTrackType(getTrackType(), samplePipelineInputBuffer.timeUs); + samplePipelineInputBuffer.timeUs -= streamOffsetUs; + samplePipelineInputBuffer.flip(); + samplePipeline.queueInputBuffer(); + return !samplePipelineInputBuffer.isEndOfStream(); case C.RESULT_FORMAT_READ: throw new IllegalStateException("Format changes are not supported."); case C.RESULT_NOTHING_READ: @@ -291,150 +162,4 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return false; } } - - /** - * 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. - */ - @RequiresNonNull({"encoderInputAudioFormat"}) - private void feedEncoder(MediaCodecAdapterWrapper encoder, ByteBuffer inputBuffer) { - ByteBuffer encoderInputBufferData = checkNotNull(encoderInputBuffer.data); - int bufferLimit = inputBuffer.limit(); - inputBuffer.limit(min(bufferLimit, inputBuffer.position() + encoderInputBufferData.capacity())); - encoderInputBufferData.put(inputBuffer); - encoderInputBuffer.timeUs = nextEncoderInputBufferTimeUs; - nextEncoderInputBufferTimeUs += - getBufferDurationUs( - /* bytesWritten= */ encoderInputBufferData.position(), - encoderInputAudioFormat.bytesPerFrame, - encoderInputAudioFormat.sampleRate); - encoderInputBuffer.setFlags(0); - encoderInputBuffer.flip(); - inputBuffer.limit(bufferLimit); - encoder.queueInputBuffer(encoderInputBuffer); - } - - private void queueEndOfStreamToEncoder(MediaCodecAdapterWrapper encoder) { - checkState(checkNotNull(encoderInputBuffer.data).position() == 0); - encoderInputBuffer.timeUs = nextEncoderInputBufferTimeUs; - encoderInputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); - encoderInputBuffer.flip(); - // Queuing EOS should only occur with an empty buffer. - encoder.queueInputBuffer(encoderInputBuffer); - } - - /** - * Attempts to configure the {@link #encoder} and Sonic (if applicable), if they have not been - * configured yet, and returns whether they have been configured. - */ - @RequiresNonNull({"decoder", "decoderInputFormat"}) - @EnsuresNonNullIf( - expression = {"encoder", "encoderInputAudioFormat"}, - result = true) - private boolean ensureEncoderAndAudioProcessingConfigured() throws ExoPlaybackException { - if (encoder != null && encoderInputAudioFormat != null) { - return true; - } - MediaCodecAdapterWrapper decoder = this.decoder; - @Nullable Format decoderOutputFormat = decoder.getOutputFormat(); - if (decoderOutputFormat == null) { - return false; - } - AudioFormat outputAudioFormat = - new AudioFormat( - decoderOutputFormat.sampleRate, - decoderOutputFormat.channelCount, - decoderOutputFormat.pcmEncoding); - if (transformation.flattenForSlowMotion) { - try { - outputAudioFormat = sonicAudioProcessor.configure(outputAudioFormat); - flushSonicAndSetSpeed(currentSpeed); - } catch (AudioProcessor.UnhandledAudioFormatException e) { - // TODO(internal b/192864511): Assign an adequate error code. - throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED); - } - } - String audioMimeType = - transformation.audioMimeType == null - ? decoderInputFormat.sampleMimeType - : transformation.audioMimeType; - try { - encoder = - MediaCodecAdapterWrapper.createForAudioEncoding( - new Format.Builder() - .setSampleMimeType(audioMimeType) - .setSampleRate(outputAudioFormat.sampleRate) - .setChannelCount(outputAudioFormat.channelCount) - .setAverageBitrate(DEFAULT_ENCODER_BITRATE) - .build()); - } catch (IOException e) { - // TODO(internal b/192864511): Assign an adequate error code. - throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED); - } - encoderInputAudioFormat = outputAudioFormat; - return true; - } - - /** - * Attempts to configure the {@link #decoder} if it has not been configured yet, and returns - * whether the decoder has been configured. - */ - @EnsuresNonNullIf( - expression = {"decoderInputFormat", "decoder"}, - result = true) - private boolean ensureDecoderConfigured() throws ExoPlaybackException { - if (decoder != null && decoderInputFormat != null) { - return true; - } - - FormatHolder formatHolder = getFormatHolder(); - @ReadDataResult int result = readSource(formatHolder, decoderInputBuffer, FLAG_REQUIRE_FORMAT); - if (result != C.RESULT_FORMAT_READ) { - return false; - } - decoderInputFormat = checkNotNull(formatHolder.format); - MediaCodecAdapterWrapper decoder; - try { - decoder = MediaCodecAdapterWrapper.createForAudioDecoding(decoderInputFormat); - } catch (IOException e) { - // TODO (internal b/184262323): Assign an adequate error code. - throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED); - } - speedProvider = new SegmentSpeedProvider(decoderInputFormat); - currentSpeed = speedProvider.getSpeed(0); - this.decoder = decoder; - return true; - } - - private boolean isSpeedChanging(BufferInfo bufferInfo) { - if (!transformation.flattenForSlowMotion) { - return false; - } - float newSpeed = checkNotNull(speedProvider).getSpeed(bufferInfo.presentationTimeUs); - boolean speedChanging = newSpeed != currentSpeed; - currentSpeed = newSpeed; - return speedChanging; - } - - private void flushSonicAndSetSpeed(float speed) { - sonicAudioProcessor.setSpeed(speed); - sonicAudioProcessor.setPitch(speed); - sonicAudioProcessor.flush(); - } - - private ExoPlaybackException createRendererException(Throwable cause, int errorCode) { - return ExoPlaybackException.createForRenderer( - cause, - TAG, - getIndex(), - decoderInputFormat, - /* rendererFormatSupport= */ C.FORMAT_HANDLED, - /* isRecoverable= */ false, - errorCode); - } - - private static long getBufferDurationUs(long bytesWritten, int bytesPerFrame, int sampleRate) { - long framesWritten = bytesWritten / bytesPerFrame; - return framesWritten * C.MICROS_PER_SECOND / sampleRate; - } } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerTranscodingVideoRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerTranscodingVideoRenderer.java index f4836e49df..30e5125f17 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerTranscodingVideoRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerTranscodingVideoRenderer.java @@ -16,33 +16,18 @@ package com.google.android.exoplayer2.transformer; +import static com.google.android.exoplayer2.source.SampleStream.FLAG_REQUIRE_FORMAT; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; import android.content.Context; -import android.graphics.SurfaceTexture; -import android.media.MediaCodec; -import android.opengl.EGL14; -import android.opengl.EGLContext; -import android.opengl.EGLDisplay; -import android.opengl.EGLExt; -import android.opengl.EGLSurface; -import android.opengl.GLES20; -import android.view.Surface; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.SampleStream; -import com.google.android.exoplayer2.util.GlUtil; -import com.google.common.collect.ImmutableMap; -import java.io.IOException; -import java.nio.ByteBuffer; -import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -50,35 +35,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @RequiresApi(18) /* package */ final class TransformerTranscodingVideoRenderer extends TransformerBaseRenderer { - static { - GlUtil.glAssertionsEnabled = true; - } - private static final String TAG = "TransformerTranscodingVideoRenderer"; private final Context context; - private final DecoderInputBuffer decoderInputBuffer; - private final float[] decoderTextureTransformMatrix; - - private @MonotonicNonNull Format decoderInputFormat; - - @Nullable private EGLDisplay eglDisplay; - @Nullable private EGLContext eglContext; - @Nullable private EGLSurface eglSurface; - - private int decoderTextureId; - @Nullable private SurfaceTexture decoderSurfaceTexture; - @Nullable private Surface decoderSurface; - @Nullable private MediaCodecAdapterWrapper decoder; - private volatile boolean isDecoderSurfacePopulated; - private boolean waitingForPopulatedDecoderSurface; - @Nullable private GlUtil.Uniform decoderTextureTransformUniform; - - @Nullable private MediaCodecAdapterWrapper encoder; - /** Whether encoder's actual output format is obtained. */ - private boolean hasEncoderActualOutputFormat; + private @MonotonicNonNull SamplePipeline samplePipeline; + private boolean muxerWrapperTrackAdded; private boolean muxerWrapperTrackEnded; public TransformerTranscodingVideoRenderer( @@ -88,9 +51,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; Transformation transformation) { super(C.TRACK_TYPE_VIDEO, muxerWrapper, mediaClock, transformation); this.context = context; - decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); - decoderTextureTransformMatrix = new float[16]; - decoderTextureId = GlUtil.TEXTURE_ID_UNSET; + decoderInputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); } @Override @@ -98,34 +60,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return TAG; } - @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (!isRendererStarted || isEnded() || !ensureInputFormatRead()) { - return; - } - ensureEncoderConfigured(); - MediaCodecAdapterWrapper encoder = this.encoder; - ensureOpenGlConfigured(); - EGLDisplay eglDisplay = this.eglDisplay; - EGLSurface eglSurface = this.eglSurface; - GlUtil.Uniform decoderTextureTransformUniform = this.decoderTextureTransformUniform; - if (!ensureDecoderConfigured()) { - return; - } - MediaCodecAdapterWrapper decoder = this.decoder; - SurfaceTexture decoderSurfaceTexture = this.decoderSurfaceTexture; - - while (feedMuxerFromEncoder(encoder)) {} - while (feedEncoderFromDecoder( - decoder, - encoder, - decoderSurfaceTexture, - eglDisplay, - eglSurface, - decoderTextureTransformUniform)) {} - while (feedDecoderFromInput(decoder)) {} - } - @Override public boolean isEnded() { return muxerWrapperTrackEnded; @@ -133,272 +67,107 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override protected void onReset() { - decoderInputBuffer.clear(); - decoderInputBuffer.data = null; - GlUtil.destroyEglContext(eglDisplay, eglContext); - eglDisplay = null; - eglContext = null; - eglSurface = null; - if (decoderTextureId != GlUtil.TEXTURE_ID_UNSET) { - GlUtil.deleteTexture(decoderTextureId); + if (samplePipeline != null) { + samplePipeline.release(); } - if (decoderSurfaceTexture != null) { - decoderSurfaceTexture.release(); - decoderSurfaceTexture = null; - } - if (decoderSurface != null) { - decoderSurface.release(); - decoderSurface = null; - } - if (decoder != null) { - decoder.release(); - decoder = null; - } - isDecoderSurfacePopulated = false; - waitingForPopulatedDecoderSurface = false; - decoderTextureTransformUniform = null; - if (encoder != null) { - encoder.release(); - encoder = null; - } - hasEncoderActualOutputFormat = false; + muxerWrapperTrackAdded = false; muxerWrapperTrackEnded = false; } - @EnsuresNonNullIf(expression = "decoderInputFormat", result = true) - private boolean ensureInputFormatRead() { - if (decoderInputFormat != null) { + @Override + public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + if (!isRendererStarted || isEnded() || !ensureRendererConfigured()) { + return; + } + + while (feedMuxerFromPipeline() || samplePipeline.processData() || feedPipelineFromInput()) {} + } + + /** Attempts to read the input format and to initialize the sample pipeline. */ + @EnsuresNonNullIf(expression = "samplePipeline", result = true) + private boolean ensureRendererConfigured() throws ExoPlaybackException { + if (samplePipeline != null) { return true; } FormatHolder formatHolder = getFormatHolder(); - @SampleStream.ReadDataResult - int result = - readSource( - formatHolder, decoderInputBuffer, /* readFlags= */ SampleStream.FLAG_REQUIRE_FORMAT); + @ReadDataResult + int result = readSource(formatHolder, decoderInputBuffer, /* readFlags= */ FLAG_REQUIRE_FORMAT); if (result != C.RESULT_FORMAT_READ) { return false; } - decoderInputFormat = checkNotNull(formatHolder.format); + Format decoderInputFormat = checkNotNull(formatHolder.format); + if (transformation.videoMimeType != null + && !transformation.videoMimeType.equals(decoderInputFormat.sampleMimeType)) { + samplePipeline = + new VideoSamplePipeline(context, decoderInputFormat, transformation, getIndex()); + } else { + samplePipeline = new PassthroughSamplePipeline(decoderInputFormat); + } return true; } - @RequiresNonNull({"decoderInputFormat"}) - @EnsuresNonNull({"encoder"}) - private void ensureEncoderConfigured() throws ExoPlaybackException { - if (encoder != null) { - return; - } - - try { - encoder = - MediaCodecAdapterWrapper.createForVideoEncoding( - new Format.Builder() - .setWidth(decoderInputFormat.width) - .setHeight(decoderInputFormat.height) - .setSampleMimeType( - transformation.videoMimeType != null - ? transformation.videoMimeType - : decoderInputFormat.sampleMimeType) - .build(), - ImmutableMap.of()); - } catch (IOException e) { - throw createRendererException( - // TODO(claincly): should be "ENCODER_INIT_FAILED" - e, decoderInputFormat, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED); - } - } - - @RequiresNonNull({"encoder", "decoderInputFormat"}) - @EnsuresNonNull({"eglDisplay", "eglSurface", "decoderTextureTransformUniform"}) - private void ensureOpenGlConfigured() { - if (eglDisplay != null && eglSurface != null && decoderTextureTransformUniform != null) { - return; - } - - MediaCodecAdapterWrapper encoder = this.encoder; - EGLDisplay eglDisplay = GlUtil.createEglDisplay(); - EGLContext eglContext; - try { - eglContext = GlUtil.createEglContext(eglDisplay); - this.eglContext = eglContext; - } catch (GlUtil.UnsupportedEglVersionException e) { - throw new IllegalStateException("EGL version is unsupported", e); - } - EGLSurface eglSurface = - GlUtil.getEglSurface(eglDisplay, checkNotNull(encoder.getInputSurface())); - GlUtil.focusSurface( - eglDisplay, eglContext, eglSurface, decoderInputFormat.width, decoderInputFormat.height); - decoderTextureId = GlUtil.createExternalTexture(); - GlUtil.Program copyProgram; - try { - copyProgram = - new GlUtil.Program( - context, - /* vertexShaderFilePath= */ "shaders/blit_vertex_shader.glsl", - /* fragmentShaderFilePath= */ "shaders/copy_external_fragment_shader.glsl"); - } catch (IOException e) { - throw new IllegalStateException(e); - } - - copyProgram.use(); - GlUtil.Attribute[] copyAttributes = copyProgram.getAttributes(); - checkState(copyAttributes.length == 2, "Expected program to have two vertex attributes."); - for (GlUtil.Attribute copyAttribute : copyAttributes) { - if (copyAttribute.name.equals("a_position")) { - copyAttribute.setBuffer( - new float[] { - -1.0f, -1.0f, 0.0f, 1.0f, - 1.0f, -1.0f, 0.0f, 1.0f, - -1.0f, 1.0f, 0.0f, 1.0f, - 1.0f, 1.0f, 0.0f, 1.0f, - }, - /* size= */ 4); - } else if (copyAttribute.name.equals("a_texcoord")) { - copyAttribute.setBuffer( - new float[] { - 0.0f, 0.0f, 0.0f, 1.0f, - 1.0f, 0.0f, 0.0f, 1.0f, - 0.0f, 1.0f, 0.0f, 1.0f, - 1.0f, 1.0f, 0.0f, 1.0f, - }, - /* size= */ 4); - } else { - throw new IllegalStateException("Unexpected attribute name."); - } - copyAttribute.bind(); - } - GlUtil.Uniform[] copyUniforms = copyProgram.getUniforms(); - checkState(copyUniforms.length == 2, "Expected program to have two uniforms."); - for (GlUtil.Uniform copyUniform : copyUniforms) { - if (copyUniform.name.equals("tex_sampler")) { - copyUniform.setSamplerTexId(decoderTextureId, 0); - copyUniform.bind(); - } else if (copyUniform.name.equals("tex_transform")) { - decoderTextureTransformUniform = copyUniform; - } else { - throw new IllegalStateException("Unexpected uniform name."); - } - } - checkNotNull(decoderTextureTransformUniform); - this.eglDisplay = eglDisplay; - this.eglSurface = eglSurface; - } - - @RequiresNonNull({"decoderInputFormat"}) - @EnsuresNonNullIf( - expression = {"decoder", "decoderSurfaceTexture"}, - result = true) - private boolean ensureDecoderConfigured() throws ExoPlaybackException { - if (decoder != null && decoderSurfaceTexture != null) { - return true; - } - - checkState(decoderTextureId != GlUtil.TEXTURE_ID_UNSET); - SurfaceTexture decoderSurfaceTexture = new SurfaceTexture(decoderTextureId); - decoderSurfaceTexture.setOnFrameAvailableListener( - surfaceTexture -> isDecoderSurfacePopulated = true); - decoderSurface = new Surface(decoderSurfaceTexture); - try { - decoder = MediaCodecAdapterWrapper.createForVideoDecoding(decoderInputFormat, decoderSurface); - } catch (IOException e) { - throw createRendererException( - e, decoderInputFormat, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED); - } - this.decoderSurfaceTexture = decoderSurfaceTexture; - return true; - } - - private boolean feedDecoderFromInput(MediaCodecAdapterWrapper decoder) { - if (!decoder.maybeDequeueInputBuffer(decoderInputBuffer)) { - return false; - } - - decoderInputBuffer.clear(); - @SampleStream.ReadDataResult - int result = readSource(getFormatHolder(), decoderInputBuffer, /* readFlags= */ 0); - switch (result) { - case C.RESULT_FORMAT_READ: - throw new IllegalStateException("Format changes are not supported."); - case C.RESULT_BUFFER_READ: - mediaClock.updateTimeForTrackType(getTrackType(), decoderInputBuffer.timeUs); - decoderInputBuffer.timeUs -= streamOffsetUs; - ByteBuffer data = checkNotNull(decoderInputBuffer.data); - data.flip(); - decoder.queueInputBuffer(decoderInputBuffer); - return !decoderInputBuffer.isEndOfStream(); - case C.RESULT_NOTHING_READ: - default: - return false; - } - } - - private boolean feedEncoderFromDecoder( - MediaCodecAdapterWrapper decoder, - MediaCodecAdapterWrapper encoder, - SurfaceTexture decoderSurfaceTexture, - EGLDisplay eglDisplay, - EGLSurface eglSurface, - GlUtil.Uniform decoderTextureTransformUniform) { - if (decoder.isEnded()) { - return false; - } - - if (!isDecoderSurfacePopulated) { - if (!waitingForPopulatedDecoderSurface) { - if (decoder.getOutputBufferInfo() != null) { - decoder.releaseOutputBuffer(/* render= */ true); - waitingForPopulatedDecoderSurface = true; - } - if (decoder.isEnded()) { - encoder.signalEndOfInputStream(); - } - } - return false; - } - - waitingForPopulatedDecoderSurface = false; - decoderSurfaceTexture.updateTexImage(); - decoderSurfaceTexture.getTransformMatrix(decoderTextureTransformMatrix); - decoderTextureTransformUniform.setFloats(decoderTextureTransformMatrix); - decoderTextureTransformUniform.bind(); - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); - long decoderSurfaceTextureTimestampNs = decoderSurfaceTexture.getTimestamp(); - EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, decoderSurfaceTextureTimestampNs); - EGL14.eglSwapBuffers(eglDisplay, eglSurface); - isDecoderSurfacePopulated = false; - return true; - } - - private boolean feedMuxerFromEncoder(MediaCodecAdapterWrapper encoder) { - if (!hasEncoderActualOutputFormat) { - @Nullable Format encoderOutputFormat = encoder.getOutputFormat(); - if (encoderOutputFormat == null) { + /** + * Attempts to write sample pipeline output data to the muxer, and returns whether it may be + * possible to write more data immediately by calling this method again. + */ + @RequiresNonNull("samplePipeline") + private boolean feedMuxerFromPipeline() { + if (!muxerWrapperTrackAdded) { + @Nullable Format samplePipelineOutputFormat = samplePipeline.getOutputFormat(); + if (samplePipelineOutputFormat == null) { return false; } - hasEncoderActualOutputFormat = true; - muxerWrapper.addTrackFormat(encoderOutputFormat); + muxerWrapperTrackAdded = true; + muxerWrapper.addTrackFormat(samplePipelineOutputFormat); } - if (encoder.isEnded()) { + if (samplePipeline.isEnded()) { muxerWrapper.endTrack(getTrackType()); muxerWrapperTrackEnded = true; return false; } - @Nullable ByteBuffer encoderOutputBuffer = encoder.getOutputBuffer(); - if (encoderOutputBuffer == null) { + @Nullable DecoderInputBuffer samplePipelineOutputBuffer = samplePipeline.getOutputBuffer(); + if (samplePipelineOutputBuffer == null) { return false; } - MediaCodec.BufferInfo encoderOutputBufferInfo = checkNotNull(encoder.getOutputBufferInfo()); if (!muxerWrapper.writeSample( getTrackType(), - encoderOutputBuffer, - /* isKeyFrame= */ (encoderOutputBufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) > 0, - encoderOutputBufferInfo.presentationTimeUs)) { + samplePipelineOutputBuffer.data, + samplePipelineOutputBuffer.isKeyFrame(), + samplePipelineOutputBuffer.timeUs)) { return false; } - encoder.releaseOutputBuffer(); + samplePipeline.releaseOutputBuffer(); return true; } + + /** + * Attempts to pass input data to the sample pipeline, and returns whether it may be possible to + * pass more data immediately by calling this method again. + */ + @RequiresNonNull("samplePipeline") + private boolean feedPipelineFromInput() { + @Nullable DecoderInputBuffer samplePipelineInputBuffer = samplePipeline.dequeueInputBuffer(); + if (samplePipelineInputBuffer == null) { + return false; + } + + @ReadDataResult + int result = readSource(getFormatHolder(), samplePipelineInputBuffer, /* readFlags= */ 0); + switch (result) { + case C.RESULT_BUFFER_READ: + mediaClock.updateTimeForTrackType(getTrackType(), samplePipelineInputBuffer.timeUs); + samplePipelineInputBuffer.timeUs -= streamOffsetUs; + samplePipelineInputBuffer.flip(); + samplePipeline.queueInputBuffer(); + return !samplePipelineInputBuffer.isEndOfStream(); + case C.RESULT_FORMAT_READ: + throw new IllegalStateException("Format changes are not supported."); + case C.RESULT_NOTHING_READ: + default: + return false; + } + } } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java new file mode 100644 index 0000000000..004331404d --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java @@ -0,0 +1,335 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.transformer; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.media.MediaCodec; +import android.opengl.EGL14; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLExt; +import android.opengl.EGLSurface; +import android.opengl.GLES20; +import android.view.Surface; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackException; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.util.GlUtil; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** + * Pipeline to decode video samples, apply transformations on the raw samples, and re-encode them. + */ +@RequiresApi(18) +/* package */ final class VideoSamplePipeline implements SamplePipeline { + + static { + GlUtil.glAssertionsEnabled = true; + } + + private static final String TAG = "VideoSamplePipeline"; + + // Predefined shader values. + private static final String VERTEX_SHADER_FILE_PATH = "shaders/blit_vertex_shader.glsl"; + private static final String FRAGMENT_SHADER_FILE_PATH = + "shaders/copy_external_fragment_shader.glsl"; + private static final int EXPECTED_NUMBER_OF_ATTRIBUTES = 2; + private static final int EXPECTED_NUMBER_OF_UNIFORMS = 2; + + private final Context context; + private final int rendererIndex; + + private final MediaCodecAdapterWrapper encoder; + private final DecoderInputBuffer encoderOutputBuffer; + + private final DecoderInputBuffer decoderInputBuffer; + private final float[] decoderTextureTransformMatrix; + private final Format decoderInputFormat; + + private @MonotonicNonNull EGLDisplay eglDisplay; + private @MonotonicNonNull EGLContext eglContext; + private @MonotonicNonNull EGLSurface eglSurface; + + private int decoderTextureId; + private @MonotonicNonNull SurfaceTexture decoderSurfaceTexture; + private @MonotonicNonNull Surface decoderSurface; + private @MonotonicNonNull MediaCodecAdapterWrapper decoder; + private volatile boolean isDecoderSurfacePopulated; + private boolean waitingForPopulatedDecoderSurface; + private GlUtil.@MonotonicNonNull Uniform decoderTextureTransformUniform; + + public VideoSamplePipeline( + Context context, Format decoderInputFormat, Transformation transformation, int rendererIndex) + throws ExoPlaybackException { + this.decoderInputFormat = decoderInputFormat; + this.rendererIndex = rendererIndex; + this.context = context; + + decoderInputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + decoderTextureTransformMatrix = new float[16]; + decoderTextureId = GlUtil.TEXTURE_ID_UNSET; + + encoderOutputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + try { + encoder = + MediaCodecAdapterWrapper.createForVideoEncoding( + new Format.Builder() + .setWidth(decoderInputFormat.width) + .setHeight(decoderInputFormat.height) + .setSampleMimeType( + transformation.videoMimeType != null + ? transformation.videoMimeType + : decoderInputFormat.sampleMimeType) + .build(), + ImmutableMap.of()); + } catch (IOException e) { + // TODO (internal b/184262323): Assign an adequate error code. + throw ExoPlaybackException.createForRenderer( + e, + TAG, + rendererIndex, + decoderInputFormat, + /* rendererFormatSupport= */ C.FORMAT_HANDLED, + /* isRecoverable= */ false, + PlaybackException.ERROR_CODE_UNSPECIFIED); + } + } + + @Override + public boolean processData() throws ExoPlaybackException { + ensureOpenGlConfigured(); + return !ensureDecoderConfigured() || feedEncoderFromDecoder(); + } + + @Override + @Nullable + public DecoderInputBuffer dequeueInputBuffer() { + return decoder != null && decoder.maybeDequeueInputBuffer(decoderInputBuffer) + ? decoderInputBuffer + : null; + } + + @Override + public void queueInputBuffer() { + checkStateNotNull(decoder).queueInputBuffer(decoderInputBuffer); + } + + @Override + @Nullable + public Format getOutputFormat() { + return encoder.getOutputFormat(); + } + + @Override + public boolean isEnded() { + return encoder.isEnded(); + } + + @Override + @Nullable + public DecoderInputBuffer getOutputBuffer() { + encoderOutputBuffer.data = encoder.getOutputBuffer(); + if (encoderOutputBuffer.data == null) { + return null; + } + MediaCodec.BufferInfo bufferInfo = checkNotNull(encoder.getOutputBufferInfo()); + encoderOutputBuffer.timeUs = bufferInfo.presentationTimeUs; + encoderOutputBuffer.setFlags(bufferInfo.flags); + return encoderOutputBuffer; + } + + @Override + public void releaseOutputBuffer() { + encoder.releaseOutputBuffer(); + } + + @Override + public void release() { + GlUtil.destroyEglContext(eglDisplay, eglContext); + if (decoderTextureId != GlUtil.TEXTURE_ID_UNSET) { + GlUtil.deleteTexture(decoderTextureId); + } + if (decoderSurfaceTexture != null) { + decoderSurfaceTexture.release(); + } + if (decoderSurface != null) { + decoderSurface.release(); + } + if (decoder != null) { + decoder.release(); + } + encoder.release(); + } + + @EnsuresNonNull({"eglDisplay", "eglContext", "eglSurface", "decoderTextureTransformUniform"}) + private void ensureOpenGlConfigured() { + if (eglDisplay != null + && eglContext != null + && eglSurface != null + && decoderTextureTransformUniform != null) { + return; + } + + eglDisplay = GlUtil.createEglDisplay(); + try { + eglContext = GlUtil.createEglContext(eglDisplay); + } catch (GlUtil.UnsupportedEglVersionException e) { + throw new IllegalStateException("EGL version is unsupported", e); + } + eglSurface = GlUtil.getEglSurface(eglDisplay, checkNotNull(encoder.getInputSurface())); + GlUtil.focusSurface( + eglDisplay, eglContext, eglSurface, decoderInputFormat.width, decoderInputFormat.height); + decoderTextureId = GlUtil.createExternalTexture(); + GlUtil.Program copyProgram; + try { + copyProgram = new GlUtil.Program(context, VERTEX_SHADER_FILE_PATH, FRAGMENT_SHADER_FILE_PATH); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + copyProgram.use(); + GlUtil.Attribute[] copyAttributes = copyProgram.getAttributes(); + checkState( + copyAttributes.length == EXPECTED_NUMBER_OF_ATTRIBUTES, + "Expected program to have " + EXPECTED_NUMBER_OF_ATTRIBUTES + " vertex attributes."); + for (GlUtil.Attribute copyAttribute : copyAttributes) { + if (copyAttribute.name.equals("a_position")) { + copyAttribute.setBuffer( + new float[] { + -1.0f, -1.0f, 0.0f, 1.0f, + 1.0f, -1.0f, 0.0f, 1.0f, + -1.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 0.0f, 1.0f, + }, + /* size= */ 4); + } else if (copyAttribute.name.equals("a_texcoord")) { + copyAttribute.setBuffer( + new float[] { + 0.0f, 0.0f, 0.0f, 1.0f, + 1.0f, 0.0f, 0.0f, 1.0f, + 0.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 0.0f, 1.0f, + }, + /* size= */ 4); + } else { + throw new IllegalStateException("Unexpected attribute name."); + } + copyAttribute.bind(); + } + GlUtil.Uniform[] copyUniforms = copyProgram.getUniforms(); + checkState( + copyUniforms.length == EXPECTED_NUMBER_OF_UNIFORMS, + "Expected program to have " + EXPECTED_NUMBER_OF_UNIFORMS + " uniforms."); + for (GlUtil.Uniform copyUniform : copyUniforms) { + if (copyUniform.name.equals("tex_sampler")) { + copyUniform.setSamplerTexId(decoderTextureId, 0); + copyUniform.bind(); + } else if (copyUniform.name.equals("tex_transform")) { + decoderTextureTransformUniform = copyUniform; + } else { + throw new IllegalStateException("Unexpected uniform name."); + } + } + checkNotNull(decoderTextureTransformUniform); + } + + @EnsuresNonNullIf( + expression = {"decoder", "decoderSurfaceTexture"}, + result = true) + private boolean ensureDecoderConfigured() throws ExoPlaybackException { + if (decoder != null && decoderSurfaceTexture != null) { + return true; + } + + checkState(decoderTextureId != GlUtil.TEXTURE_ID_UNSET); + decoderSurfaceTexture = new SurfaceTexture(decoderTextureId); + decoderSurfaceTexture.setOnFrameAvailableListener( + surfaceTexture -> isDecoderSurfacePopulated = true); + decoderSurface = new Surface(decoderSurfaceTexture); + try { + decoder = MediaCodecAdapterWrapper.createForVideoDecoding(decoderInputFormat, decoderSurface); + } catch (IOException e) { + throw createRendererException(e, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED); + } + return true; + } + + @RequiresNonNull({ + "decoder", + "decoderSurfaceTexture", + "decoderTextureTransformUniform", + "eglDisplay", + "eglSurface" + }) + private boolean feedEncoderFromDecoder() { + if (decoder.isEnded()) { + return false; + } + + if (!isDecoderSurfacePopulated) { + if (!waitingForPopulatedDecoderSurface) { + if (decoder.getOutputBufferInfo() != null) { + decoder.releaseOutputBuffer(/* render= */ true); + waitingForPopulatedDecoderSurface = true; + } + if (decoder.isEnded()) { + encoder.signalEndOfInputStream(); + } + } + return false; + } + + waitingForPopulatedDecoderSurface = false; + decoderSurfaceTexture.updateTexImage(); + decoderSurfaceTexture.getTransformMatrix(decoderTextureTransformMatrix); + decoderTextureTransformUniform.setFloats(decoderTextureTransformMatrix); + decoderTextureTransformUniform.bind(); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + long decoderSurfaceTextureTimestampNs = decoderSurfaceTexture.getTimestamp(); + EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, decoderSurfaceTextureTimestampNs); + EGL14.eglSwapBuffers(eglDisplay, eglSurface); + isDecoderSurfacePopulated = false; + return true; + } + + private ExoPlaybackException createRendererException(Throwable cause, int errorCode) { + return ExoPlaybackException.createForRenderer( + cause, + TAG, + rendererIndex, + decoderInputFormat, + /* rendererFormatSupport= */ C.FORMAT_HANDLED, + /* isRecoverable= */ false, + errorCode); + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index c6e0aadad0..cd33ecc9d9 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -82,7 +82,6 @@ import java.util.Formatter; import java.util.List; import java.util.Locale; import java.util.concurrent.CopyOnWriteArrayList; -import org.checkerframework.dataflow.qual.Pure; /** * A view for controlling {@link Player} instances. @@ -2166,12 +2165,11 @@ public class StyledPlayerControlView extends FrameLayout { TrackSelectionParameters trackSelectionParameters = player.getTrackSelectionParameters(); TrackSelectionOverrides overrides = - forceTrackSelection( - trackSelectionParameters.trackSelectionOverrides, - track.tracksInfo, - track.trackGroupIndex, - new TrackSelectionOverride( - track.trackGroup, ImmutableList.of(track.trackIndex))); + new TrackSelectionOverrides.Builder() + .setOverrideForType( + new TrackSelectionOverride( + track.trackGroup, ImmutableList.of(track.trackIndex))) + .build(); checkNotNull(player) .setTrackSelectionParameters( trackSelectionParameters @@ -2209,41 +2207,4 @@ public class StyledPlayerControlView extends FrameLayout { checkView = itemView.findViewById(R.id.exo_check); } } - - /** - * Forces tracks in a {@link TrackGroup} to be the only ones selected for a {@link C.TrackType}. - * No other tracks of that type will be selectable. If the forced tracks are not supported, then - * no tracks of that type will be selected. - * - * @param trackSelectionOverrides The current {@link TrackSelectionOverride overrides}. - * @param tracksInfo The current {@link TracksInfo}. - * @param forcedTrackGroupIndex The index of the {@link TrackGroup} in {@code tracksInfo} that - * should have its track selected. - * @param forcedTrackSelectionOverride The tracks to force selection of. - * @return The updated {@link TrackSelectionOverride overrides}. - */ - @Pure - private static TrackSelectionOverrides forceTrackSelection( - TrackSelectionOverrides trackSelectionOverrides, - TracksInfo tracksInfo, - int forcedTrackGroupIndex, - TrackSelectionOverride forcedTrackSelectionOverride) { - TrackSelectionOverrides.Builder overridesBuilder = trackSelectionOverrides.buildUpon(); - - @C.TrackType - int trackType = tracksInfo.getTrackGroupInfos().get(forcedTrackGroupIndex).getTrackType(); - overridesBuilder.setOverrideForType(forcedTrackSelectionOverride); - // TrackSelectionOverride doesn't currently guarantee that only overwritten track - // group of a given type are selected, so the others have to be explicitly disabled. - // This guarantee is provided in the following patch that removes the need for this method. - ImmutableList trackGroupInfos = tracksInfo.getTrackGroupInfos(); - for (int i = 0; i < trackGroupInfos.size(); i++) { - TrackGroupInfo trackGroupInfo = trackGroupInfos.get(i); - if (i != forcedTrackGroupIndex && trackGroupInfo.getTrackType() == trackType) { - TrackGroup trackGroup = trackGroupInfo.getTrackGroup(); - overridesBuilder.addOverride(new TrackSelectionOverride(trackGroup, ImmutableList.of())); - } - } - return overridesBuilder.build(); - } } diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.0.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.0.dump new file mode 100644 index 0000000000..1e129acd70 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.0.dump @@ -0,0 +1,36 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 66936 + sample count = 4 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 19200, hash EF6C7C27 + sample 1: + time = 100000 + flags = 1 + data = length 19200, hash 5AB97AFC + sample 2: + time = 200000 + flags = 1 + data = length 19200, hash 37920F33 + sample 3: + time = 300000 + flags = 1 + data = length 9336, hash 135F1C30 +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.1.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.1.dump new file mode 100644 index 0000000000..f4d925bad3 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.1.dump @@ -0,0 +1,32 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 44628 + sample count = 3 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 116208 + flags = 1 + data = length 19200, hash E4B962ED + sample 1: + time = 216208 + flags = 1 + data = length 19200, hash 4F13D6CF + sample 2: + time = 316208 + flags = 1 + data = length 6228, hash 3FB5F446 +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.2.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.2.dump new file mode 100644 index 0000000000..2b42e93b5a --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.2.dump @@ -0,0 +1,28 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 22316 + sample count = 2 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 232416 + flags = 1 + data = length 19200, hash F82E494B + sample 1: + time = 332416 + flags = 1 + data = length 3116, hash 93C99CFD +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.3.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.3.dump new file mode 100644 index 0000000000..2a6345d4a8 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.3.dump @@ -0,0 +1,24 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 4 + sample count = 1 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 348625 + flags = 1 + data = length 4, hash FFD4C53F +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.unknown_length.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.unknown_length.dump new file mode 100644 index 0000000000..1e129acd70 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.unknown_length.dump @@ -0,0 +1,36 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 66936 + sample count = 4 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 19200, hash EF6C7C27 + sample 1: + time = 100000 + flags = 1 + data = length 19200, hash 5AB97AFC + sample 2: + time = 200000 + flags = 1 + data = length 19200, hash 37920F33 + sample 3: + time = 300000 + flags = 1 + data = length 9336, hash 135F1C30 +tracksEnded = true diff --git a/testdata/src/test/assets/media/wav/sample_rf64.wav b/testdata/src/test/assets/media/wav/sample_rf64.wav new file mode 100644 index 0000000000..b2dd53c687 Binary files /dev/null and b/testdata/src/test/assets/media/wav/sample_rf64.wav differ diff --git a/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.dump b/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.dump index 18836cbc5d..1b6fd750ad 100644 --- a/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.dump +++ b/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.dump @@ -1,9 +1,9 @@ containerMimeType = video/mp4 format 0: sampleMimeType = audio/3gpp + maxInputSize = 61 channelCount = 1 sampleRate = 8000 - pcmEncoding = 2 sample: trackIndex = 0 dataHashCode = 924517484 @@ -15,1301 +15,1301 @@ sample: dataHashCode = -835666085 size = 13 isKeyFrame = true - presentationTimeUs = 750 + presentationTimeUs = 20000 sample: trackIndex = 0 dataHashCode = 430283125 size = 13 isKeyFrame = true - presentationTimeUs = 1500 + presentationTimeUs = 40000 sample: trackIndex = 0 dataHashCode = 1215919932 size = 13 isKeyFrame = true - presentationTimeUs = 2250 + presentationTimeUs = 60000 sample: trackIndex = 0 dataHashCode = -386387943 size = 13 isKeyFrame = true - presentationTimeUs = 3000 + presentationTimeUs = 80000 sample: trackIndex = 0 dataHashCode = -765080119 size = 13 isKeyFrame = true - presentationTimeUs = 3750 + presentationTimeUs = 100000 sample: trackIndex = 0 dataHashCode = -1855636054 size = 13 isKeyFrame = true - presentationTimeUs = 4500 + presentationTimeUs = 120000 sample: trackIndex = 0 dataHashCode = -946579722 size = 13 isKeyFrame = true - presentationTimeUs = 5250 + presentationTimeUs = 140000 sample: trackIndex = 0 dataHashCode = -841202654 size = 13 isKeyFrame = true - presentationTimeUs = 6000 + presentationTimeUs = 160000 sample: trackIndex = 0 dataHashCode = -638764303 size = 13 isKeyFrame = true - presentationTimeUs = 6750 + presentationTimeUs = 180000 sample: trackIndex = 0 dataHashCode = -1162388941 size = 13 isKeyFrame = true - presentationTimeUs = 7500 + presentationTimeUs = 200000 sample: trackIndex = 0 dataHashCode = 572634367 size = 13 isKeyFrame = true - presentationTimeUs = 8250 + presentationTimeUs = 220000 sample: trackIndex = 0 dataHashCode = -1774188021 size = 13 isKeyFrame = true - presentationTimeUs = 9000 + presentationTimeUs = 240000 sample: trackIndex = 0 dataHashCode = 92464891 size = 13 isKeyFrame = true - presentationTimeUs = 9750 + presentationTimeUs = 260000 sample: trackIndex = 0 dataHashCode = -991397659 size = 13 isKeyFrame = true - presentationTimeUs = 10500 + presentationTimeUs = 280000 sample: trackIndex = 0 dataHashCode = -934698563 size = 13 isKeyFrame = true - presentationTimeUs = 11250 + presentationTimeUs = 300000 sample: trackIndex = 0 dataHashCode = -811030035 size = 13 isKeyFrame = true - presentationTimeUs = 12000 + presentationTimeUs = 320000 sample: trackIndex = 0 dataHashCode = 1892305159 size = 13 isKeyFrame = true - presentationTimeUs = 12750 + presentationTimeUs = 340000 sample: trackIndex = 0 dataHashCode = -1266858924 size = 13 isKeyFrame = true - presentationTimeUs = 13500 + presentationTimeUs = 360000 sample: trackIndex = 0 dataHashCode = 673814721 size = 13 isKeyFrame = true - presentationTimeUs = 14250 + presentationTimeUs = 380000 sample: trackIndex = 0 dataHashCode = 1061124709 size = 13 isKeyFrame = true - presentationTimeUs = 15000 + presentationTimeUs = 400000 sample: trackIndex = 0 dataHashCode = -869356712 size = 13 isKeyFrame = true - presentationTimeUs = 15750 + presentationTimeUs = 420000 sample: trackIndex = 0 dataHashCode = 664729362 size = 13 isKeyFrame = true - presentationTimeUs = 16500 + presentationTimeUs = 440000 sample: trackIndex = 0 dataHashCode = -1439741143 size = 13 isKeyFrame = true - presentationTimeUs = 17250 + presentationTimeUs = 460000 sample: trackIndex = 0 dataHashCode = -151627580 size = 13 isKeyFrame = true - presentationTimeUs = 18000 + presentationTimeUs = 480000 sample: trackIndex = 0 dataHashCode = -673268457 size = 13 isKeyFrame = true - presentationTimeUs = 18750 + presentationTimeUs = 500000 sample: trackIndex = 0 dataHashCode = 1839962647 size = 13 isKeyFrame = true - presentationTimeUs = 19500 + presentationTimeUs = 520000 sample: trackIndex = 0 dataHashCode = 1858999665 size = 13 isKeyFrame = true - presentationTimeUs = 20250 + presentationTimeUs = 540000 sample: trackIndex = 0 dataHashCode = -1278193537 size = 13 isKeyFrame = true - presentationTimeUs = 21000 + presentationTimeUs = 560000 sample: trackIndex = 0 dataHashCode = 568547001 size = 13 isKeyFrame = true - presentationTimeUs = 21750 + presentationTimeUs = 580000 sample: trackIndex = 0 dataHashCode = 68217362 size = 13 isKeyFrame = true - presentationTimeUs = 22500 + presentationTimeUs = 600000 sample: trackIndex = 0 dataHashCode = 1396217256 size = 13 isKeyFrame = true - presentationTimeUs = 23250 + presentationTimeUs = 620000 sample: trackIndex = 0 dataHashCode = -971293094 size = 13 isKeyFrame = true - presentationTimeUs = 24000 + presentationTimeUs = 640000 sample: trackIndex = 0 dataHashCode = -1742638874 size = 13 isKeyFrame = true - presentationTimeUs = 24750 + presentationTimeUs = 660000 sample: trackIndex = 0 dataHashCode = 2047109317 size = 13 isKeyFrame = true - presentationTimeUs = 25500 + presentationTimeUs = 680000 sample: trackIndex = 0 dataHashCode = -1668945241 size = 13 isKeyFrame = true - presentationTimeUs = 26250 + presentationTimeUs = 700000 sample: trackIndex = 0 dataHashCode = -1229766218 size = 13 isKeyFrame = true - presentationTimeUs = 27000 + presentationTimeUs = 720000 sample: trackIndex = 0 dataHashCode = 1765233454 size = 13 isKeyFrame = true - presentationTimeUs = 27750 + presentationTimeUs = 740000 sample: trackIndex = 0 dataHashCode = -1930255456 size = 13 isKeyFrame = true - presentationTimeUs = 28500 + presentationTimeUs = 760000 sample: trackIndex = 0 dataHashCode = -764925242 size = 13 isKeyFrame = true - presentationTimeUs = 29250 + presentationTimeUs = 780000 sample: trackIndex = 0 dataHashCode = -1144688369 size = 13 isKeyFrame = true - presentationTimeUs = 30000 + presentationTimeUs = 800000 sample: trackIndex = 0 dataHashCode = 1493699436 size = 13 isKeyFrame = true - presentationTimeUs = 30750 + presentationTimeUs = 820000 sample: trackIndex = 0 dataHashCode = -468614511 size = 13 isKeyFrame = true - presentationTimeUs = 31500 + presentationTimeUs = 840000 sample: trackIndex = 0 dataHashCode = -1578782058 size = 13 isKeyFrame = true - presentationTimeUs = 32250 + presentationTimeUs = 860000 sample: trackIndex = 0 dataHashCode = -675743397 size = 13 isKeyFrame = true - presentationTimeUs = 33000 + presentationTimeUs = 880000 sample: trackIndex = 0 dataHashCode = -863790111 size = 13 isKeyFrame = true - presentationTimeUs = 33750 + presentationTimeUs = 900000 sample: trackIndex = 0 dataHashCode = -732307506 size = 13 isKeyFrame = true - presentationTimeUs = 34500 + presentationTimeUs = 920000 sample: trackIndex = 0 dataHashCode = -693298708 size = 13 isKeyFrame = true - presentationTimeUs = 35250 + presentationTimeUs = 940000 sample: trackIndex = 0 dataHashCode = -799131843 size = 13 isKeyFrame = true - presentationTimeUs = 36000 + presentationTimeUs = 960000 sample: trackIndex = 0 dataHashCode = 1782866119 size = 13 isKeyFrame = true - presentationTimeUs = 36750 + presentationTimeUs = 980000 sample: trackIndex = 0 dataHashCode = -912205505 size = 13 isKeyFrame = true - presentationTimeUs = 37500 + presentationTimeUs = 1000000 sample: trackIndex = 0 dataHashCode = 1067981287 size = 13 isKeyFrame = true - presentationTimeUs = 38250 + presentationTimeUs = 1020000 sample: trackIndex = 0 dataHashCode = 490520060 size = 13 isKeyFrame = true - presentationTimeUs = 39000 + presentationTimeUs = 1040000 sample: trackIndex = 0 dataHashCode = -1950632957 size = 13 isKeyFrame = true - presentationTimeUs = 39750 + presentationTimeUs = 1060000 sample: trackIndex = 0 dataHashCode = 565485817 size = 13 isKeyFrame = true - presentationTimeUs = 40500 + presentationTimeUs = 1080000 sample: trackIndex = 0 dataHashCode = -1057414703 size = 13 isKeyFrame = true - presentationTimeUs = 41250 + presentationTimeUs = 1100000 sample: trackIndex = 0 dataHashCode = 1568746155 size = 13 isKeyFrame = true - presentationTimeUs = 42000 + presentationTimeUs = 1120000 sample: trackIndex = 0 dataHashCode = 1355412472 size = 13 isKeyFrame = true - presentationTimeUs = 42750 + presentationTimeUs = 1140000 sample: trackIndex = 0 dataHashCode = 1546368465 size = 13 isKeyFrame = true - presentationTimeUs = 43500 + presentationTimeUs = 1160000 sample: trackIndex = 0 dataHashCode = 1811529381 size = 13 isKeyFrame = true - presentationTimeUs = 44250 + presentationTimeUs = 1180000 sample: trackIndex = 0 dataHashCode = 658031078 size = 13 isKeyFrame = true - presentationTimeUs = 45000 + presentationTimeUs = 1200000 sample: trackIndex = 0 dataHashCode = 1606584486 size = 13 isKeyFrame = true - presentationTimeUs = 45750 + presentationTimeUs = 1220000 sample: trackIndex = 0 dataHashCode = 2123252778 size = 13 isKeyFrame = true - presentationTimeUs = 46500 + presentationTimeUs = 1240000 sample: trackIndex = 0 dataHashCode = -1364579398 size = 13 isKeyFrame = true - presentationTimeUs = 47250 + presentationTimeUs = 1260000 sample: trackIndex = 0 dataHashCode = 1311427887 size = 13 isKeyFrame = true - presentationTimeUs = 48000 + presentationTimeUs = 1280000 sample: trackIndex = 0 dataHashCode = -691467569 size = 13 isKeyFrame = true - presentationTimeUs = 48750 + presentationTimeUs = 1300000 sample: trackIndex = 0 dataHashCode = 1876470084 size = 13 isKeyFrame = true - presentationTimeUs = 49500 + presentationTimeUs = 1320000 sample: trackIndex = 0 dataHashCode = -1472873479 size = 13 isKeyFrame = true - presentationTimeUs = 50250 + presentationTimeUs = 1340000 sample: trackIndex = 0 dataHashCode = -143574992 size = 13 isKeyFrame = true - presentationTimeUs = 51000 + presentationTimeUs = 1360000 sample: trackIndex = 0 dataHashCode = 984180453 size = 13 isKeyFrame = true - presentationTimeUs = 51750 + presentationTimeUs = 1380000 sample: trackIndex = 0 dataHashCode = -113645527 size = 13 isKeyFrame = true - presentationTimeUs = 52500 + presentationTimeUs = 1400000 sample: trackIndex = 0 dataHashCode = 1987501641 size = 13 isKeyFrame = true - presentationTimeUs = 53250 + presentationTimeUs = 1420000 sample: trackIndex = 0 dataHashCode = -1816426230 size = 13 isKeyFrame = true - presentationTimeUs = 54000 + presentationTimeUs = 1440000 sample: trackIndex = 0 dataHashCode = -1250050360 size = 13 isKeyFrame = true - presentationTimeUs = 54750 + presentationTimeUs = 1460000 sample: trackIndex = 0 dataHashCode = 1722852790 size = 13 isKeyFrame = true - presentationTimeUs = 55500 + presentationTimeUs = 1480000 sample: trackIndex = 0 dataHashCode = 225656333 size = 13 isKeyFrame = true - presentationTimeUs = 56250 + presentationTimeUs = 1500000 sample: trackIndex = 0 dataHashCode = -2137778394 size = 13 isKeyFrame = true - presentationTimeUs = 57000 + presentationTimeUs = 1520000 sample: trackIndex = 0 dataHashCode = 1433327155 size = 13 isKeyFrame = true - presentationTimeUs = 57750 + presentationTimeUs = 1540000 sample: trackIndex = 0 dataHashCode = -974261023 size = 13 isKeyFrame = true - presentationTimeUs = 58500 + presentationTimeUs = 1560000 sample: trackIndex = 0 dataHashCode = 1797813317 size = 13 isKeyFrame = true - presentationTimeUs = 59250 + presentationTimeUs = 1580000 sample: trackIndex = 0 dataHashCode = -594033497 size = 13 isKeyFrame = true - presentationTimeUs = 60000 + presentationTimeUs = 1600000 sample: trackIndex = 0 dataHashCode = -628310540 size = 13 isKeyFrame = true - presentationTimeUs = 60750 + presentationTimeUs = 1620000 sample: trackIndex = 0 dataHashCode = 1868627831 size = 13 isKeyFrame = true - presentationTimeUs = 61500 + presentationTimeUs = 1640000 sample: trackIndex = 0 dataHashCode = 1051863958 size = 13 isKeyFrame = true - presentationTimeUs = 62250 + presentationTimeUs = 1660000 sample: trackIndex = 0 dataHashCode = -1279059211 size = 13 isKeyFrame = true - presentationTimeUs = 63000 + presentationTimeUs = 1680000 sample: trackIndex = 0 dataHashCode = 408201874 size = 13 isKeyFrame = true - presentationTimeUs = 63750 + presentationTimeUs = 1700000 sample: trackIndex = 0 dataHashCode = 1686644299 size = 13 isKeyFrame = true - presentationTimeUs = 64500 + presentationTimeUs = 1720000 sample: trackIndex = 0 dataHashCode = 1288226241 size = 13 isKeyFrame = true - presentationTimeUs = 65250 + presentationTimeUs = 1740000 sample: trackIndex = 0 dataHashCode = 432829731 size = 13 isKeyFrame = true - presentationTimeUs = 66000 + presentationTimeUs = 1760000 sample: trackIndex = 0 dataHashCode = -1679312600 size = 13 isKeyFrame = true - presentationTimeUs = 66750 + presentationTimeUs = 1780000 sample: trackIndex = 0 dataHashCode = 1206680829 size = 13 isKeyFrame = true - presentationTimeUs = 67500 + presentationTimeUs = 1800000 sample: trackIndex = 0 dataHashCode = -325844704 size = 13 isKeyFrame = true - presentationTimeUs = 68250 + presentationTimeUs = 1820000 sample: trackIndex = 0 dataHashCode = 1941808848 size = 13 isKeyFrame = true - presentationTimeUs = 69000 + presentationTimeUs = 1840000 sample: trackIndex = 0 dataHashCode = -87346412 size = 13 isKeyFrame = true - presentationTimeUs = 69750 + presentationTimeUs = 1860000 sample: trackIndex = 0 dataHashCode = -329133765 size = 13 isKeyFrame = true - presentationTimeUs = 70500 + presentationTimeUs = 1880000 sample: trackIndex = 0 dataHashCode = -1299416212 size = 13 isKeyFrame = true - presentationTimeUs = 71250 + presentationTimeUs = 1900000 sample: trackIndex = 0 dataHashCode = -1314599219 size = 13 isKeyFrame = true - presentationTimeUs = 72000 + presentationTimeUs = 1920000 sample: trackIndex = 0 dataHashCode = 1456741286 size = 13 isKeyFrame = true - presentationTimeUs = 72750 + presentationTimeUs = 1940000 sample: trackIndex = 0 dataHashCode = 151296500 size = 13 isKeyFrame = true - presentationTimeUs = 73500 + presentationTimeUs = 1960000 sample: trackIndex = 0 dataHashCode = 1708763603 size = 13 isKeyFrame = true - presentationTimeUs = 74250 + presentationTimeUs = 1980000 sample: trackIndex = 0 dataHashCode = 227542220 size = 13 isKeyFrame = true - presentationTimeUs = 75000 + presentationTimeUs = 2000000 sample: trackIndex = 0 dataHashCode = 1094305517 size = 13 isKeyFrame = true - presentationTimeUs = 75750 + presentationTimeUs = 2020000 sample: trackIndex = 0 dataHashCode = -990377604 size = 13 isKeyFrame = true - presentationTimeUs = 76500 + presentationTimeUs = 2040000 sample: trackIndex = 0 dataHashCode = -1798036230 size = 13 isKeyFrame = true - presentationTimeUs = 77250 + presentationTimeUs = 2060000 sample: trackIndex = 0 dataHashCode = -1027148291 size = 13 isKeyFrame = true - presentationTimeUs = 78000 + presentationTimeUs = 2080000 sample: trackIndex = 0 dataHashCode = 359763976 size = 13 isKeyFrame = true - presentationTimeUs = 78750 + presentationTimeUs = 2100000 sample: trackIndex = 0 dataHashCode = 1332016420 size = 13 isKeyFrame = true - presentationTimeUs = 79500 + presentationTimeUs = 2120000 sample: trackIndex = 0 dataHashCode = -102753250 size = 13 isKeyFrame = true - presentationTimeUs = 80250 + presentationTimeUs = 2140000 sample: trackIndex = 0 dataHashCode = 1959063156 size = 13 isKeyFrame = true - presentationTimeUs = 81000 + presentationTimeUs = 2160000 sample: trackIndex = 0 dataHashCode = 2129089853 size = 13 isKeyFrame = true - presentationTimeUs = 81750 + presentationTimeUs = 2180000 sample: trackIndex = 0 dataHashCode = 1658742073 size = 13 isKeyFrame = true - presentationTimeUs = 82500 + presentationTimeUs = 2200000 sample: trackIndex = 0 dataHashCode = 2136916514 size = 13 isKeyFrame = true - presentationTimeUs = 83250 + presentationTimeUs = 2220000 sample: trackIndex = 0 dataHashCode = 105121407 size = 13 isKeyFrame = true - presentationTimeUs = 84000 + presentationTimeUs = 2240000 sample: trackIndex = 0 dataHashCode = -839464484 size = 13 isKeyFrame = true - presentationTimeUs = 84750 + presentationTimeUs = 2260000 sample: trackIndex = 0 dataHashCode = -1956791168 size = 13 isKeyFrame = true - presentationTimeUs = 85500 + presentationTimeUs = 2280000 sample: trackIndex = 0 dataHashCode = -1387546109 size = 13 isKeyFrame = true - presentationTimeUs = 86250 + presentationTimeUs = 2300000 sample: trackIndex = 0 dataHashCode = 128410432 size = 13 isKeyFrame = true - presentationTimeUs = 87000 + presentationTimeUs = 2320000 sample: trackIndex = 0 dataHashCode = 907081136 size = 13 isKeyFrame = true - presentationTimeUs = 87750 + presentationTimeUs = 2340000 sample: trackIndex = 0 dataHashCode = 1124845067 size = 13 isKeyFrame = true - presentationTimeUs = 88500 + presentationTimeUs = 2360000 sample: trackIndex = 0 dataHashCode = -1714479962 size = 13 isKeyFrame = true - presentationTimeUs = 89250 + presentationTimeUs = 2380000 sample: trackIndex = 0 dataHashCode = 322029323 size = 13 isKeyFrame = true - presentationTimeUs = 90000 + presentationTimeUs = 2400000 sample: trackIndex = 0 dataHashCode = -1116281187 size = 13 isKeyFrame = true - presentationTimeUs = 90750 + presentationTimeUs = 2420000 sample: trackIndex = 0 dataHashCode = 1571181228 size = 13 isKeyFrame = true - presentationTimeUs = 91500 + presentationTimeUs = 2440000 sample: trackIndex = 0 dataHashCode = 997979854 size = 13 isKeyFrame = true - presentationTimeUs = 92250 + presentationTimeUs = 2460000 sample: trackIndex = 0 dataHashCode = -1413492413 size = 13 isKeyFrame = true - presentationTimeUs = 93000 + presentationTimeUs = 2480000 sample: trackIndex = 0 dataHashCode = -381390490 size = 13 isKeyFrame = true - presentationTimeUs = 93750 + presentationTimeUs = 2500000 sample: trackIndex = 0 dataHashCode = -331348340 size = 13 isKeyFrame = true - presentationTimeUs = 94500 + presentationTimeUs = 2520000 sample: trackIndex = 0 dataHashCode = -1568238592 size = 13 isKeyFrame = true - presentationTimeUs = 95250 + presentationTimeUs = 2540000 sample: trackIndex = 0 dataHashCode = -941591445 size = 13 isKeyFrame = true - presentationTimeUs = 96000 + presentationTimeUs = 2560000 sample: trackIndex = 0 dataHashCode = 1616911281 size = 13 isKeyFrame = true - presentationTimeUs = 96750 + presentationTimeUs = 2580000 sample: trackIndex = 0 dataHashCode = -1755664741 size = 13 isKeyFrame = true - presentationTimeUs = 97500 + presentationTimeUs = 2600000 sample: trackIndex = 0 dataHashCode = -1950609742 size = 13 isKeyFrame = true - presentationTimeUs = 98250 + presentationTimeUs = 2620000 sample: trackIndex = 0 dataHashCode = 1476082149 size = 13 isKeyFrame = true - presentationTimeUs = 99000 + presentationTimeUs = 2640000 sample: trackIndex = 0 dataHashCode = 1289547483 size = 13 isKeyFrame = true - presentationTimeUs = 99750 + presentationTimeUs = 2660000 sample: trackIndex = 0 dataHashCode = -367599018 size = 13 isKeyFrame = true - presentationTimeUs = 100500 + presentationTimeUs = 2680000 sample: trackIndex = 0 dataHashCode = 679378334 size = 13 isKeyFrame = true - presentationTimeUs = 101250 + presentationTimeUs = 2700000 sample: trackIndex = 0 dataHashCode = 1437306809 size = 13 isKeyFrame = true - presentationTimeUs = 102000 + presentationTimeUs = 2720000 sample: trackIndex = 0 dataHashCode = 311988463 size = 13 isKeyFrame = true - presentationTimeUs = 102750 + presentationTimeUs = 2740000 sample: trackIndex = 0 dataHashCode = -1870442665 size = 13 isKeyFrame = true - presentationTimeUs = 103500 + presentationTimeUs = 2760000 sample: trackIndex = 0 dataHashCode = 1530013920 size = 13 isKeyFrame = true - presentationTimeUs = 104250 + presentationTimeUs = 2780000 sample: trackIndex = 0 dataHashCode = -585506443 size = 13 isKeyFrame = true - presentationTimeUs = 105000 + presentationTimeUs = 2800000 sample: trackIndex = 0 dataHashCode = -293690558 size = 13 isKeyFrame = true - presentationTimeUs = 105750 + presentationTimeUs = 2820000 sample: trackIndex = 0 dataHashCode = -616893325 size = 13 isKeyFrame = true - presentationTimeUs = 106500 + presentationTimeUs = 2840000 sample: trackIndex = 0 dataHashCode = 632210495 size = 13 isKeyFrame = true - presentationTimeUs = 107250 + presentationTimeUs = 2860000 sample: trackIndex = 0 dataHashCode = -291767937 size = 13 isKeyFrame = true - presentationTimeUs = 108000 + presentationTimeUs = 2880000 sample: trackIndex = 0 dataHashCode = -270265 size = 13 isKeyFrame = true - presentationTimeUs = 108750 + presentationTimeUs = 2900000 sample: trackIndex = 0 dataHashCode = -1095959376 size = 13 isKeyFrame = true - presentationTimeUs = 109500 + presentationTimeUs = 2920000 sample: trackIndex = 0 dataHashCode = -1363867284 size = 13 isKeyFrame = true - presentationTimeUs = 110250 + presentationTimeUs = 2940000 sample: trackIndex = 0 dataHashCode = 185415707 size = 13 isKeyFrame = true - presentationTimeUs = 111000 + presentationTimeUs = 2960000 sample: trackIndex = 0 dataHashCode = 1033720098 size = 13 isKeyFrame = true - presentationTimeUs = 111750 + presentationTimeUs = 2980000 sample: trackIndex = 0 dataHashCode = 1813896085 size = 13 isKeyFrame = true - presentationTimeUs = 112500 + presentationTimeUs = 3000000 sample: trackIndex = 0 dataHashCode = -1381192241 size = 13 isKeyFrame = true - presentationTimeUs = 113250 + presentationTimeUs = 3020000 sample: trackIndex = 0 dataHashCode = 362689054 size = 13 isKeyFrame = true - presentationTimeUs = 114000 + presentationTimeUs = 3040000 sample: trackIndex = 0 dataHashCode = -1320787356 size = 13 isKeyFrame = true - presentationTimeUs = 114750 + presentationTimeUs = 3060000 sample: trackIndex = 0 dataHashCode = 1306489379 size = 13 isKeyFrame = true - presentationTimeUs = 115500 + presentationTimeUs = 3080000 sample: trackIndex = 0 dataHashCode = -910313430 size = 13 isKeyFrame = true - presentationTimeUs = 116250 + presentationTimeUs = 3100000 sample: trackIndex = 0 dataHashCode = -1533334115 size = 13 isKeyFrame = true - presentationTimeUs = 117000 + presentationTimeUs = 3120000 sample: trackIndex = 0 dataHashCode = -700061723 size = 13 isKeyFrame = true - presentationTimeUs = 117750 + presentationTimeUs = 3140000 sample: trackIndex = 0 dataHashCode = 474100444 size = 13 isKeyFrame = true - presentationTimeUs = 118500 + presentationTimeUs = 3160000 sample: trackIndex = 0 dataHashCode = -2096659943 size = 13 isKeyFrame = true - presentationTimeUs = 119250 + presentationTimeUs = 3180000 sample: trackIndex = 0 dataHashCode = -690442126 size = 13 isKeyFrame = true - presentationTimeUs = 120000 + presentationTimeUs = 3200000 sample: trackIndex = 0 dataHashCode = 158718784 size = 13 isKeyFrame = true - presentationTimeUs = 120750 + presentationTimeUs = 3220000 sample: trackIndex = 0 dataHashCode = -1587553019 size = 13 isKeyFrame = true - presentationTimeUs = 121500 + presentationTimeUs = 3240000 sample: trackIndex = 0 dataHashCode = 1266916929 size = 13 isKeyFrame = true - presentationTimeUs = 122250 + presentationTimeUs = 3260000 sample: trackIndex = 0 dataHashCode = 1947792537 size = 13 isKeyFrame = true - presentationTimeUs = 123000 + presentationTimeUs = 3280000 sample: trackIndex = 0 dataHashCode = 2051622372 size = 13 isKeyFrame = true - presentationTimeUs = 123750 + presentationTimeUs = 3300000 sample: trackIndex = 0 dataHashCode = 1648973196 size = 13 isKeyFrame = true - presentationTimeUs = 124500 + presentationTimeUs = 3320000 sample: trackIndex = 0 dataHashCode = -1119069213 size = 13 isKeyFrame = true - presentationTimeUs = 125250 + presentationTimeUs = 3340000 sample: trackIndex = 0 dataHashCode = -1162670307 size = 13 isKeyFrame = true - presentationTimeUs = 126000 + presentationTimeUs = 3360000 sample: trackIndex = 0 dataHashCode = 505180178 size = 13 isKeyFrame = true - presentationTimeUs = 126750 + presentationTimeUs = 3380000 sample: trackIndex = 0 dataHashCode = -1707111799 size = 13 isKeyFrame = true - presentationTimeUs = 127500 + presentationTimeUs = 3400000 sample: trackIndex = 0 dataHashCode = 549350779 size = 13 isKeyFrame = true - presentationTimeUs = 128250 + presentationTimeUs = 3420000 sample: trackIndex = 0 dataHashCode = -895461091 size = 13 isKeyFrame = true - presentationTimeUs = 129000 + presentationTimeUs = 3440000 sample: trackIndex = 0 dataHashCode = 1834306839 size = 13 isKeyFrame = true - presentationTimeUs = 129750 + presentationTimeUs = 3460000 sample: trackIndex = 0 dataHashCode = -646169807 size = 13 isKeyFrame = true - presentationTimeUs = 130500 + presentationTimeUs = 3480000 sample: trackIndex = 0 dataHashCode = 123454915 size = 13 isKeyFrame = true - presentationTimeUs = 131250 + presentationTimeUs = 3500000 sample: trackIndex = 0 dataHashCode = 2074179659 size = 13 isKeyFrame = true - presentationTimeUs = 132000 + presentationTimeUs = 3520000 sample: trackIndex = 0 dataHashCode = 488070546 size = 13 isKeyFrame = true - presentationTimeUs = 132750 + presentationTimeUs = 3540000 sample: trackIndex = 0 dataHashCode = -1379245827 size = 13 isKeyFrame = true - presentationTimeUs = 133500 + presentationTimeUs = 3560000 sample: trackIndex = 0 dataHashCode = 922846867 size = 13 isKeyFrame = true - presentationTimeUs = 134250 + presentationTimeUs = 3580000 sample: trackIndex = 0 dataHashCode = 1163092079 size = 13 isKeyFrame = true - presentationTimeUs = 135000 + presentationTimeUs = 3600000 sample: trackIndex = 0 dataHashCode = -817674907 size = 13 isKeyFrame = true - presentationTimeUs = 135750 + presentationTimeUs = 3620000 sample: trackIndex = 0 dataHashCode = -765143209 size = 13 isKeyFrame = true - presentationTimeUs = 136500 + presentationTimeUs = 3640000 sample: trackIndex = 0 dataHashCode = 1337234415 size = 13 isKeyFrame = true - presentationTimeUs = 137250 + presentationTimeUs = 3660000 sample: trackIndex = 0 dataHashCode = 152696122 size = 13 isKeyFrame = true - presentationTimeUs = 138000 + presentationTimeUs = 3680000 sample: trackIndex = 0 dataHashCode = -1037369189 size = 13 isKeyFrame = true - presentationTimeUs = 138750 + presentationTimeUs = 3700000 sample: trackIndex = 0 dataHashCode = 93852784 size = 13 isKeyFrame = true - presentationTimeUs = 139500 + presentationTimeUs = 3720000 sample: trackIndex = 0 dataHashCode = -1512860804 size = 13 isKeyFrame = true - presentationTimeUs = 140250 + presentationTimeUs = 3740000 sample: trackIndex = 0 dataHashCode = -1571797975 size = 13 isKeyFrame = true - presentationTimeUs = 141000 + presentationTimeUs = 3760000 sample: trackIndex = 0 dataHashCode = -1390710594 size = 13 isKeyFrame = true - presentationTimeUs = 141750 + presentationTimeUs = 3780000 sample: trackIndex = 0 dataHashCode = 775548254 size = 13 isKeyFrame = true - presentationTimeUs = 142500 + presentationTimeUs = 3800000 sample: trackIndex = 0 dataHashCode = 329825934 size = 13 isKeyFrame = true - presentationTimeUs = 143250 + presentationTimeUs = 3820000 sample: trackIndex = 0 dataHashCode = 449672203 size = 13 isKeyFrame = true - presentationTimeUs = 144000 + presentationTimeUs = 3840000 sample: trackIndex = 0 dataHashCode = 135215283 size = 13 isKeyFrame = true - presentationTimeUs = 144750 + presentationTimeUs = 3860000 sample: trackIndex = 0 dataHashCode = -627202145 size = 13 isKeyFrame = true - presentationTimeUs = 145500 + presentationTimeUs = 3880000 sample: trackIndex = 0 dataHashCode = 565795710 size = 13 isKeyFrame = true - presentationTimeUs = 146250 + presentationTimeUs = 3900000 sample: trackIndex = 0 dataHashCode = -853390981 size = 13 isKeyFrame = true - presentationTimeUs = 147000 + presentationTimeUs = 3920000 sample: trackIndex = 0 dataHashCode = 1904980829 size = 13 isKeyFrame = true - presentationTimeUs = 147750 + presentationTimeUs = 3940000 sample: trackIndex = 0 dataHashCode = 1772857005 size = 13 isKeyFrame = true - presentationTimeUs = 148500 + presentationTimeUs = 3960000 sample: trackIndex = 0 dataHashCode = -1159621303 size = 13 isKeyFrame = true - presentationTimeUs = 149250 + presentationTimeUs = 3980000 sample: trackIndex = 0 dataHashCode = 712585139 size = 13 isKeyFrame = true - presentationTimeUs = 150000 + presentationTimeUs = 4000000 sample: trackIndex = 0 dataHashCode = 7470296 size = 13 isKeyFrame = true - presentationTimeUs = 150750 + presentationTimeUs = 4020000 sample: trackIndex = 0 dataHashCode = 1154659763 size = 13 isKeyFrame = true - presentationTimeUs = 151500 + presentationTimeUs = 4040000 sample: trackIndex = 0 dataHashCode = 512209179 size = 13 isKeyFrame = true - presentationTimeUs = 152250 + presentationTimeUs = 4060000 sample: trackIndex = 0 dataHashCode = 2026712081 size = 13 isKeyFrame = true - presentationTimeUs = 153000 + presentationTimeUs = 4080000 sample: trackIndex = 0 dataHashCode = -1625715216 size = 13 isKeyFrame = true - presentationTimeUs = 153750 + presentationTimeUs = 4100000 sample: trackIndex = 0 dataHashCode = -1299058326 size = 13 isKeyFrame = true - presentationTimeUs = 154500 + presentationTimeUs = 4120000 sample: trackIndex = 0 dataHashCode = -813560096 size = 13 isKeyFrame = true - presentationTimeUs = 155250 + presentationTimeUs = 4140000 sample: trackIndex = 0 dataHashCode = 1311045251 size = 13 isKeyFrame = true - presentationTimeUs = 156000 + presentationTimeUs = 4160000 sample: trackIndex = 0 dataHashCode = 1388107407 size = 13 isKeyFrame = true - presentationTimeUs = 156750 + presentationTimeUs = 4180000 sample: trackIndex = 0 dataHashCode = 1113099440 size = 13 isKeyFrame = true - presentationTimeUs = 157500 + presentationTimeUs = 4200000 sample: trackIndex = 0 dataHashCode = -339743582 size = 13 isKeyFrame = true - presentationTimeUs = 158250 + presentationTimeUs = 4220000 sample: trackIndex = 0 dataHashCode = -1055895345 size = 13 isKeyFrame = true - presentationTimeUs = 159000 + presentationTimeUs = 4240000 sample: trackIndex = 0 dataHashCode = 1869841923 size = 13 isKeyFrame = true - presentationTimeUs = 159750 + presentationTimeUs = 4260000 sample: trackIndex = 0 dataHashCode = 229443301 size = 13 isKeyFrame = true - presentationTimeUs = 160500 + presentationTimeUs = 4280000 sample: trackIndex = 0 dataHashCode = 1526951012 size = 13 isKeyFrame = true - presentationTimeUs = 161250 + presentationTimeUs = 4300000 sample: trackIndex = 0 dataHashCode = -1517436626 size = 13 isKeyFrame = true - presentationTimeUs = 162000 + presentationTimeUs = 4320000 sample: trackIndex = 0 dataHashCode = -1403405700 size = 13 isKeyFrame = true - presentationTimeUs = 162750 + presentationTimeUs = 4340000 released = true diff --git a/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump index dd820ba6ab..7b6604be43 100644 --- a/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump +++ b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump @@ -1,9 +1,15 @@ containerMimeType = video/mp4 format 0: + id = 2 sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 channelCount = 1 sampleRate = 44100 - pcmEncoding = 2 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 format 1: id = 1 sampleMimeType = video/avc @@ -110,181 +116,127 @@ sample: dataHashCode = 1205768497 size = 23 isKeyFrame = true - presentationTimeUs = 0 + presentationTimeUs = 44000 sample: trackIndex = 0 dataHashCode = 837571078 size = 6 isKeyFrame = true - presentationTimeUs = 249 + presentationTimeUs = 67219 sample: trackIndex = 0 dataHashCode = -1991633045 size = 148 isKeyFrame = true - presentationTimeUs = 317 + presentationTimeUs = 90439 sample: trackIndex = 0 dataHashCode = -822987359 size = 189 isKeyFrame = true - presentationTimeUs = 1995 + presentationTimeUs = 113659 sample: trackIndex = 0 dataHashCode = -1141508176 size = 205 isKeyFrame = true - presentationTimeUs = 4126 + presentationTimeUs = 136879 sample: trackIndex = 0 dataHashCode = -226971245 size = 210 isKeyFrame = true - presentationTimeUs = 6438 + presentationTimeUs = 160099 sample: trackIndex = 0 dataHashCode = -2099636855 size = 210 isKeyFrame = true - presentationTimeUs = 8818 + presentationTimeUs = 183319 sample: trackIndex = 0 dataHashCode = 1541550559 size = 207 isKeyFrame = true - presentationTimeUs = 11198 + presentationTimeUs = 206539 sample: trackIndex = 0 dataHashCode = 411148001 size = 225 isKeyFrame = true - presentationTimeUs = 13533 + presentationTimeUs = 229759 sample: trackIndex = 0 dataHashCode = -897603973 size = 215 isKeyFrame = true - presentationTimeUs = 16072 + presentationTimeUs = 252979 sample: trackIndex = 0 dataHashCode = 1478106136 size = 211 isKeyFrame = true - presentationTimeUs = 18498 + presentationTimeUs = 276199 sample: trackIndex = 0 dataHashCode = -1380417145 size = 216 isKeyFrame = true - presentationTimeUs = 20878 + presentationTimeUs = 299419 sample: trackIndex = 0 dataHashCode = 780903644 size = 229 isKeyFrame = true - presentationTimeUs = 23326 + presentationTimeUs = 322639 sample: trackIndex = 0 dataHashCode = 586204432 size = 232 isKeyFrame = true - presentationTimeUs = 25911 + presentationTimeUs = 345859 sample: trackIndex = 0 dataHashCode = -2038771492 size = 235 isKeyFrame = true - presentationTimeUs = 28541 + presentationTimeUs = 369079 sample: trackIndex = 0 dataHashCode = -2065161304 size = 231 isKeyFrame = true - presentationTimeUs = 31194 + presentationTimeUs = 392299 sample: trackIndex = 0 dataHashCode = 468662933 size = 226 isKeyFrame = true - presentationTimeUs = 33801 + presentationTimeUs = 415519 sample: trackIndex = 0 dataHashCode = -358398546 size = 216 isKeyFrame = true - presentationTimeUs = 36363 + presentationTimeUs = 438739 sample: trackIndex = 0 dataHashCode = 1767325983 size = 229 isKeyFrame = true - presentationTimeUs = 38811 + presentationTimeUs = 461959 sample: trackIndex = 0 dataHashCode = 1093095458 size = 219 isKeyFrame = true - presentationTimeUs = 41396 + presentationTimeUs = 485179 sample: trackIndex = 0 dataHashCode = 1687543702 size = 241 isKeyFrame = true - presentationTimeUs = 43867 -sample: - trackIndex = 0 - dataHashCode = 1675188486 - size = 228 - isKeyFrame = true - presentationTimeUs = 46588 -sample: - trackIndex = 0 - dataHashCode = 888567545 - size = 238 - isKeyFrame = true - presentationTimeUs = 49173 -sample: - trackIndex = 0 - dataHashCode = -439631803 - size = 234 - isKeyFrame = true - presentationTimeUs = 51871 -sample: - trackIndex = 0 - dataHashCode = 1606694497 - size = 231 - isKeyFrame = true - presentationTimeUs = 54524 -sample: - trackIndex = 0 - dataHashCode = 1747388653 - size = 217 - isKeyFrame = true - presentationTimeUs = 57131 -sample: - trackIndex = 0 - dataHashCode = -734560004 - size = 239 - isKeyFrame = true - presentationTimeUs = 59579 -sample: - trackIndex = 0 - dataHashCode = -975079040 - size = 243 - isKeyFrame = true - presentationTimeUs = 62277 -sample: - trackIndex = 0 - dataHashCode = -1403504710 - size = 231 - isKeyFrame = true - presentationTimeUs = 65020 -sample: - trackIndex = 0 - dataHashCode = 379512981 - size = 230 - isKeyFrame = true - presentationTimeUs = 67627 + presentationTimeUs = 508399 sample: trackIndex = 1 dataHashCode = -1830836678 @@ -309,96 +261,6 @@ sample: size = 4725 isKeyFrame = false presentationTimeUs = 700700 -sample: - trackIndex = 0 - dataHashCode = -997198863 - size = 238 - isKeyFrame = true - presentationTimeUs = 70234 -sample: - trackIndex = 0 - dataHashCode = 1394492825 - size = 225 - isKeyFrame = true - presentationTimeUs = 72932 -sample: - trackIndex = 0 - dataHashCode = -885232755 - size = 232 - isKeyFrame = true - presentationTimeUs = 75471 -sample: - trackIndex = 0 - dataHashCode = 260871367 - size = 243 - isKeyFrame = true - presentationTimeUs = 78101 -sample: - trackIndex = 0 - dataHashCode = -1505318960 - size = 232 - isKeyFrame = true - presentationTimeUs = 80844 -sample: - trackIndex = 0 - dataHashCode = -390625371 - size = 237 - isKeyFrame = true - presentationTimeUs = 83474 -sample: - trackIndex = 0 - dataHashCode = 1067950751 - size = 228 - isKeyFrame = true - presentationTimeUs = 86149 -sample: - trackIndex = 0 - dataHashCode = -1179436278 - size = 235 - isKeyFrame = true - presentationTimeUs = 88734 -sample: - trackIndex = 0 - dataHashCode = 1906607774 - size = 264 - isKeyFrame = true - presentationTimeUs = 91387 -sample: - trackIndex = 0 - dataHashCode = -800475828 - size = 257 - isKeyFrame = true - presentationTimeUs = 94380 -sample: - trackIndex = 0 - dataHashCode = 1718972977 - size = 227 - isKeyFrame = true - presentationTimeUs = 97282 -sample: - trackIndex = 0 - dataHashCode = -1120448741 - size = 227 - isKeyFrame = true - presentationTimeUs = 99844 -sample: - trackIndex = 0 - dataHashCode = -1718323210 - size = 235 - isKeyFrame = true - presentationTimeUs = 102406 -sample: - trackIndex = 0 - dataHashCode = -422416 - size = 229 - isKeyFrame = true - presentationTimeUs = 105059 -sample: - trackIndex = 0 - dataHashCode = 833757830 - size = 6 - isKeyFrame = true - presentationTimeUs = 107644 sample: trackIndex = 1 dataHashCode = 1569455924 @@ -465,4 +327,148 @@ sample: size = 568 isKeyFrame = false presentationTimeUs = 934266 +sample: + trackIndex = 0 + dataHashCode = 1675188486 + size = 228 + isKeyFrame = true + presentationTimeUs = 531619 +sample: + trackIndex = 0 + dataHashCode = 888567545 + size = 238 + isKeyFrame = true + presentationTimeUs = 554839 +sample: + trackIndex = 0 + dataHashCode = -439631803 + size = 234 + isKeyFrame = true + presentationTimeUs = 578058 +sample: + trackIndex = 0 + dataHashCode = 1606694497 + size = 231 + isKeyFrame = true + presentationTimeUs = 601278 +sample: + trackIndex = 0 + dataHashCode = 1747388653 + size = 217 + isKeyFrame = true + presentationTimeUs = 624498 +sample: + trackIndex = 0 + dataHashCode = -734560004 + size = 239 + isKeyFrame = true + presentationTimeUs = 647718 +sample: + trackIndex = 0 + dataHashCode = -975079040 + size = 243 + isKeyFrame = true + presentationTimeUs = 670938 +sample: + trackIndex = 0 + dataHashCode = -1403504710 + size = 231 + isKeyFrame = true + presentationTimeUs = 694158 +sample: + trackIndex = 0 + dataHashCode = 379512981 + size = 230 + isKeyFrame = true + presentationTimeUs = 717378 +sample: + trackIndex = 0 + dataHashCode = -997198863 + size = 238 + isKeyFrame = true + presentationTimeUs = 740598 +sample: + trackIndex = 0 + dataHashCode = 1394492825 + size = 225 + isKeyFrame = true + presentationTimeUs = 763818 +sample: + trackIndex = 0 + dataHashCode = -885232755 + size = 232 + isKeyFrame = true + presentationTimeUs = 787038 +sample: + trackIndex = 0 + dataHashCode = 260871367 + size = 243 + isKeyFrame = true + presentationTimeUs = 810258 +sample: + trackIndex = 0 + dataHashCode = -1505318960 + size = 232 + isKeyFrame = true + presentationTimeUs = 833478 +sample: + trackIndex = 0 + dataHashCode = -390625371 + size = 237 + isKeyFrame = true + presentationTimeUs = 856698 +sample: + trackIndex = 0 + dataHashCode = 1067950751 + size = 228 + isKeyFrame = true + presentationTimeUs = 879918 +sample: + trackIndex = 0 + dataHashCode = -1179436278 + size = 235 + isKeyFrame = true + presentationTimeUs = 903138 +sample: + trackIndex = 0 + dataHashCode = 1906607774 + size = 264 + isKeyFrame = true + presentationTimeUs = 926358 +sample: + trackIndex = 0 + dataHashCode = -800475828 + size = 257 + isKeyFrame = true + presentationTimeUs = 949578 +sample: + trackIndex = 0 + dataHashCode = 1718972977 + size = 227 + isKeyFrame = true + presentationTimeUs = 972798 +sample: + trackIndex = 0 + dataHashCode = -1120448741 + size = 227 + isKeyFrame = true + presentationTimeUs = 996018 +sample: + trackIndex = 0 + dataHashCode = -1718323210 + size = 235 + isKeyFrame = true + presentationTimeUs = 1019238 +sample: + trackIndex = 0 + dataHashCode = -422416 + size = 229 + isKeyFrame = true + presentationTimeUs = 1042458 +sample: + trackIndex = 0 + dataHashCode = 833757830 + size = 6 + isKeyFrame = true + presentationTimeUs = 1065678 released = true diff --git a/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump index e94ff8bb7f..adc14a43a1 100644 --- a/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump +++ b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump @@ -1,277 +1,283 @@ containerMimeType = video/mp4 format 0: + id = 2 sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 channelCount = 1 sampleRate = 44100 - pcmEncoding = 2 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 sample: trackIndex = 0 dataHashCode = 1205768497 size = 23 isKeyFrame = true - presentationTimeUs = 0 + presentationTimeUs = 44000 sample: trackIndex = 0 dataHashCode = 837571078 size = 6 isKeyFrame = true - presentationTimeUs = 249 + presentationTimeUs = 67219 sample: trackIndex = 0 dataHashCode = -1991633045 size = 148 isKeyFrame = true - presentationTimeUs = 317 + presentationTimeUs = 90439 sample: trackIndex = 0 dataHashCode = -822987359 size = 189 isKeyFrame = true - presentationTimeUs = 1995 + presentationTimeUs = 113659 sample: trackIndex = 0 dataHashCode = -1141508176 size = 205 isKeyFrame = true - presentationTimeUs = 4126 + presentationTimeUs = 136879 sample: trackIndex = 0 dataHashCode = -226971245 size = 210 isKeyFrame = true - presentationTimeUs = 6438 + presentationTimeUs = 160099 sample: trackIndex = 0 dataHashCode = -2099636855 size = 210 isKeyFrame = true - presentationTimeUs = 8818 + presentationTimeUs = 183319 sample: trackIndex = 0 dataHashCode = 1541550559 size = 207 isKeyFrame = true - presentationTimeUs = 11198 + presentationTimeUs = 206539 sample: trackIndex = 0 dataHashCode = 411148001 size = 225 isKeyFrame = true - presentationTimeUs = 13533 + presentationTimeUs = 229759 sample: trackIndex = 0 dataHashCode = -897603973 size = 215 isKeyFrame = true - presentationTimeUs = 16072 + presentationTimeUs = 252979 sample: trackIndex = 0 dataHashCode = 1478106136 size = 211 isKeyFrame = true - presentationTimeUs = 18498 + presentationTimeUs = 276199 sample: trackIndex = 0 dataHashCode = -1380417145 size = 216 isKeyFrame = true - presentationTimeUs = 20878 + presentationTimeUs = 299419 sample: trackIndex = 0 dataHashCode = 780903644 size = 229 isKeyFrame = true - presentationTimeUs = 23326 + presentationTimeUs = 322639 sample: trackIndex = 0 dataHashCode = 586204432 size = 232 isKeyFrame = true - presentationTimeUs = 25911 + presentationTimeUs = 345859 sample: trackIndex = 0 dataHashCode = -2038771492 size = 235 isKeyFrame = true - presentationTimeUs = 28541 + presentationTimeUs = 369079 sample: trackIndex = 0 dataHashCode = -2065161304 size = 231 isKeyFrame = true - presentationTimeUs = 31194 + presentationTimeUs = 392299 sample: trackIndex = 0 dataHashCode = 468662933 size = 226 isKeyFrame = true - presentationTimeUs = 33801 + presentationTimeUs = 415519 sample: trackIndex = 0 dataHashCode = -358398546 size = 216 isKeyFrame = true - presentationTimeUs = 36363 + presentationTimeUs = 438739 sample: trackIndex = 0 dataHashCode = 1767325983 size = 229 isKeyFrame = true - presentationTimeUs = 38811 + presentationTimeUs = 461959 sample: trackIndex = 0 dataHashCode = 1093095458 size = 219 isKeyFrame = true - presentationTimeUs = 41396 + presentationTimeUs = 485179 sample: trackIndex = 0 dataHashCode = 1687543702 size = 241 isKeyFrame = true - presentationTimeUs = 43867 + presentationTimeUs = 508399 sample: trackIndex = 0 dataHashCode = 1675188486 size = 228 isKeyFrame = true - presentationTimeUs = 46588 + presentationTimeUs = 531619 sample: trackIndex = 0 dataHashCode = 888567545 size = 238 isKeyFrame = true - presentationTimeUs = 49173 + presentationTimeUs = 554839 sample: trackIndex = 0 dataHashCode = -439631803 size = 234 isKeyFrame = true - presentationTimeUs = 51871 + presentationTimeUs = 578058 sample: trackIndex = 0 dataHashCode = 1606694497 size = 231 isKeyFrame = true - presentationTimeUs = 54524 + presentationTimeUs = 601278 sample: trackIndex = 0 dataHashCode = 1747388653 size = 217 isKeyFrame = true - presentationTimeUs = 57131 + presentationTimeUs = 624498 sample: trackIndex = 0 dataHashCode = -734560004 size = 239 isKeyFrame = true - presentationTimeUs = 59579 + presentationTimeUs = 647718 sample: trackIndex = 0 dataHashCode = -975079040 size = 243 isKeyFrame = true - presentationTimeUs = 62277 + presentationTimeUs = 670938 sample: trackIndex = 0 dataHashCode = -1403504710 size = 231 isKeyFrame = true - presentationTimeUs = 65020 + presentationTimeUs = 694158 sample: trackIndex = 0 dataHashCode = 379512981 size = 230 isKeyFrame = true - presentationTimeUs = 67627 + presentationTimeUs = 717378 sample: trackIndex = 0 dataHashCode = -997198863 size = 238 isKeyFrame = true - presentationTimeUs = 70234 + presentationTimeUs = 740598 sample: trackIndex = 0 dataHashCode = 1394492825 size = 225 isKeyFrame = true - presentationTimeUs = 72932 + presentationTimeUs = 763818 sample: trackIndex = 0 dataHashCode = -885232755 size = 232 isKeyFrame = true - presentationTimeUs = 75471 + presentationTimeUs = 787038 sample: trackIndex = 0 dataHashCode = 260871367 size = 243 isKeyFrame = true - presentationTimeUs = 78101 + presentationTimeUs = 810258 sample: trackIndex = 0 dataHashCode = -1505318960 size = 232 isKeyFrame = true - presentationTimeUs = 80844 + presentationTimeUs = 833478 sample: trackIndex = 0 dataHashCode = -390625371 size = 237 isKeyFrame = true - presentationTimeUs = 83474 + presentationTimeUs = 856698 sample: trackIndex = 0 dataHashCode = 1067950751 size = 228 isKeyFrame = true - presentationTimeUs = 86149 + presentationTimeUs = 879918 sample: trackIndex = 0 dataHashCode = -1179436278 size = 235 isKeyFrame = true - presentationTimeUs = 88734 + presentationTimeUs = 903138 sample: trackIndex = 0 dataHashCode = 1906607774 size = 264 isKeyFrame = true - presentationTimeUs = 91387 + presentationTimeUs = 926358 sample: trackIndex = 0 dataHashCode = -800475828 size = 257 isKeyFrame = true - presentationTimeUs = 94380 + presentationTimeUs = 949578 sample: trackIndex = 0 dataHashCode = 1718972977 size = 227 isKeyFrame = true - presentationTimeUs = 97282 + presentationTimeUs = 972798 sample: trackIndex = 0 dataHashCode = -1120448741 size = 227 isKeyFrame = true - presentationTimeUs = 99844 + presentationTimeUs = 996018 sample: trackIndex = 0 dataHashCode = -1718323210 size = 235 isKeyFrame = true - presentationTimeUs = 102406 + presentationTimeUs = 1019238 sample: trackIndex = 0 dataHashCode = -422416 size = 229 isKeyFrame = true - presentationTimeUs = 105059 + presentationTimeUs = 1042458 sample: trackIndex = 0 dataHashCode = 833757830 size = 6 isKeyFrame = true - presentationTimeUs = 107644 + presentationTimeUs = 1065678 released = true diff --git a/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump b/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump index 6115358157..816e26e384 100644 --- a/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump +++ b/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump @@ -132,64 +132,148 @@ sample: presentationTimeUs = 0 sample: trackIndex = 0 - dataHashCode = -833872563 - size = 1732 + dataHashCode = 1000136444 + size = 140 isKeyFrame = true - presentationTimeUs = 416 + presentationTimeUs = 417 sample: trackIndex = 0 - dataHashCode = -135901925 - size = 380 + dataHashCode = 217961709 + size = 172 isKeyFrame = true - presentationTimeUs = 36499 + presentationTimeUs = 3334 +sample: + trackIndex = 0 + dataHashCode = -879376936 + size = 176 + isKeyFrame = true + presentationTimeUs = 6917 +sample: + trackIndex = 0 + dataHashCode = 1259979587 + size = 192 + isKeyFrame = true + presentationTimeUs = 10584 +sample: + trackIndex = 0 + dataHashCode = 907407225 + size = 188 + isKeyFrame = true + presentationTimeUs = 14584 +sample: + trackIndex = 0 + dataHashCode = -904354707 + size = 176 + isKeyFrame = true + presentationTimeUs = 18500 +sample: + trackIndex = 0 + dataHashCode = 1001385853 + size = 172 + isKeyFrame = true + presentationTimeUs = 22167 +sample: + trackIndex = 0 + dataHashCode = 1545716086 + size = 196 + isKeyFrame = true + presentationTimeUs = 25750 +sample: + trackIndex = 0 + dataHashCode = 358710839 + size = 180 + isKeyFrame = true + presentationTimeUs = 29834 +sample: + trackIndex = 0 + dataHashCode = -671124798 + size = 140 + isKeyFrame = true + presentationTimeUs = 33584 +sample: + trackIndex = 0 + dataHashCode = -945404910 + size = 120 + isKeyFrame = true + presentationTimeUs = 36500 +sample: + trackIndex = 0 + dataHashCode = 1881048379 + size = 88 + isKeyFrame = true + presentationTimeUs = 39000 +sample: + trackIndex = 0 + dataHashCode = 1059579897 + size = 88 + isKeyFrame = true + presentationTimeUs = 40834 +sample: + trackIndex = 0 + dataHashCode = 1496098648 + size = 84 + isKeyFrame = true + presentationTimeUs = 42667 sample: trackIndex = 0 dataHashCode = 250093960 size = 751 isKeyFrame = true - presentationTimeUs = 44415 + presentationTimeUs = 44417 sample: trackIndex = 0 dataHashCode = 1895536226 size = 1045 isKeyFrame = true - presentationTimeUs = 59998 + presentationTimeUs = 60063 sample: trackIndex = 0 dataHashCode = 1723596464 size = 947 isKeyFrame = true - presentationTimeUs = 81748 + presentationTimeUs = 81834 sample: trackIndex = 0 dataHashCode = -978803114 size = 946 isKeyFrame = true - presentationTimeUs = 101414 + presentationTimeUs = 101563 sample: trackIndex = 0 dataHashCode = 387377078 size = 946 isKeyFrame = true - presentationTimeUs = 121080 + presentationTimeUs = 121271 sample: trackIndex = 0 dataHashCode = -132658698 size = 901 isKeyFrame = true - presentationTimeUs = 140746 + presentationTimeUs = 140980 sample: trackIndex = 0 dataHashCode = 1495036471 size = 899 isKeyFrame = true - presentationTimeUs = 159496 + presentationTimeUs = 159750 sample: trackIndex = 0 dataHashCode = 304440590 size = 878 isKeyFrame = true - presentationTimeUs = 178162 + presentationTimeUs = 178480 +sample: + trackIndex = 0 + dataHashCode = -1955900344 + size = 112 + isKeyFrame = true + presentationTimeUs = 196771 +sample: + trackIndex = 0 + dataHashCode = 88896626 + size = 116 + isKeyFrame = true + presentationTimeUs = 199105 sample: trackIndex = 1 dataHashCode = 2139021989 @@ -214,12 +298,6 @@ sample: size = 1193 isKeyFrame = false presentationTimeUs = 734083 -sample: - trackIndex = 0 - dataHashCode = -752661703 - size = 228 - isKeyFrame = true - presentationTimeUs = 196412 sample: trackIndex = 1 dataHashCode = -1554795381 diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java index b119512840..07faf15a6d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java @@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.util.Assertions.checkArgument; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -39,10 +40,12 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Map; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.junit.Ignore; import org.junit.Rule; @@ -503,6 +506,62 @@ public abstract class DataSourceContractTest { assertThat(dataSource.getUri()).isNull(); } + @Test + public void getResponseHeaders_noNullKeysOrValues() throws Exception { + ImmutableList resources = getTestResources(); + Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + + for (int i = 0; i < resources.size(); i++) { + additionalFailureInfo.setInfo(getFailureLabel(resources, i)); + TestResource resource = resources.get(i); + DataSource dataSource = createDataSource(); + try { + dataSource.open(new DataSpec(resource.getUri())); + + Map> responseHeaders = dataSource.getResponseHeaders(); + assertThat(responseHeaders).doesNotContainKey(null); + assertThat(responseHeaders.values()).doesNotContain(null); + for (List value : responseHeaders.values()) { + assertThat(value).doesNotContain(null); + } + } finally { + dataSource.close(); + } + additionalFailureInfo.setInfo(null); + } + } + + @Test + public void getResponseHeaders_caseInsensitive() throws Exception { + ImmutableList resources = getTestResources(); + Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + + for (int i = 0; i < resources.size(); i++) { + additionalFailureInfo.setInfo(getFailureLabel(resources, i)); + TestResource resource = resources.get(i); + DataSource dataSource = createDataSource(); + try { + dataSource.open(new DataSpec(resource.getUri())); + + Map> responseHeaders = dataSource.getResponseHeaders(); + for (String key : responseHeaders.keySet()) { + // TODO(internal b/205811776): Remove this when DefaultHttpDataSource is fixed to not + // return a null key. + if (key == null) { + continue; + } + String caseFlippedKey = invertAsciiCaseOfEveryOtherCharacter(key); + assertWithMessage("key='%s', caseFlippedKey='%s'", key, caseFlippedKey) + .that(responseHeaders.get(caseFlippedKey)) + .isEqualTo(responseHeaders.get(key)); + } + } finally { + dataSource.close(); + } + additionalFailureInfo.setInfo(null); + } + } + @Test public void getResponseHeaders_isEmptyWhileNotOpen() throws Exception { ImmutableList resources = getTestResources(); @@ -548,6 +607,28 @@ public abstract class DataSourceContractTest { } } + private static String invertAsciiCaseOfEveryOtherCharacter(String input) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + result.append(i % 2 == 0 ? invertAsciiCase(input.charAt(i)) : input.charAt(i)); + } + return result.toString(); + } + + /** + * Returns {@code c} in the opposite case if it's an ASCII character, otherwise returns {@code c} + * unchanged. + */ + private static char invertAsciiCase(char c) { + if (Ascii.isUpperCase(c)) { + return Ascii.toLowerCase(c); + } else if (Ascii.isLowerCase(c)) { + return Ascii.toUpperCase(c); + } else { + return c; + } + } + /** Information about a resource that can be used to test the {@link DataSource} instance. */ public static final class TestResource { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 3bb4e0562d..747957feb1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -26,6 +26,7 @@ import android.os.Looper; import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaPeriod; @@ -115,7 +116,8 @@ public class MediaSourceTestRunner { final IOException[] prepareError = new IOException[1]; runOnPlaybackThread( () -> { - mediaSource.prepareSource(mediaSourceListener, /* mediaTransferListener= */ null); + mediaSource.prepareSource( + mediaSourceListener, /* mediaTransferListener= */ null, PlayerId.UNSET); try { // TODO: This only catches errors that are set synchronously in prepareSource. To // capture async errors we'll need to poll maybeThrowSourceInfoRefreshError until the diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/WebServerDispatcher.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/WebServerDispatcher.java index ee21cea874..3b9a6fe0f0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/WebServerDispatcher.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/WebServerDispatcher.java @@ -261,7 +261,7 @@ public class WebServerDispatcher extends Dispatcher { Resource resource = checkNotNull(resourcesByPath.get(requestPath)); byte[] resourceData = resource.getData(); if (resource.supportsRangeRequests()) { - response.setHeader("Accept-ranges", "bytes"); + response.setHeader("Accept-Ranges", "bytes"); } @Nullable ImmutableMap acceptEncodingHeader = getAcceptEncodingHeader(request); @Nullable String preferredContentCoding; diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactoryTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactoryTest.java index c9f9a5a7b6..ac2c41ccf7 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactoryTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactoryTest.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.MediaSource; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; @@ -42,7 +43,8 @@ public class FakeMediaSourceFactoryTest { int firstWindowIndex = timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false); reportedMediaItem.set(timeline.getWindow(firstWindowIndex, new Window()).mediaItem); }, - /* mediaTransferListener= */ null); + /* mediaTransferListener= */ null, + PlayerId.UNSET); assertThat(reportedMediaItem.get()).isSameInstanceAs(mediaItem); assertThat(mediaSource.getMediaItem()).isSameInstanceAs(mediaItem);