Merge pull request #120 from androidx/release-1.0

r1.0.0 beta02
This commit is contained in:
Rohit Kumar Singh 2022-07-22 09:31:34 +00:00 committed by GitHub
commit ca6835bf0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 3698 additions and 570 deletions

View File

@ -17,6 +17,7 @@ body:
label: Media3 Version
description: What version of Media3 are you using?
options:
- 1.0.0-beta02
- 1.0.0-beta01
- 1.0.0-alpha03
- 1.0.0-alpha02

View File

@ -1,13 +1,47 @@
Release notes
### Unreleased changes
### 1.0.0-beta02 (2022-07-21)
This release corresponds to the
[ExoPlayer 2.18.1 release](https://github.com/google/ExoPlayer/releases/tag/r2.18.1).
* Core library:
* Ensure that changing the `ShuffleOrder` with `ExoPlayer.setShuffleOrder`
results in a call to `Player.Listener#onTimelineChanged` with
`reason=Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED`
([#9889](https://github.com/google/ExoPlayer/issues/9889)).
* For progressive media, only include selected tracks in buffered position
([#10361](https://github.com/google/ExoPlayer/issues/10361)).
* Allow custom logger for all ExoPlayer log output
([#9752](https://github.com/google/ExoPlayer/issues/9752)).
* Fix implementation of `setDataSourceFactory` in
`DefaultMediaSourceFactory`, which was non-functional in some cases
([#116](https://github.com/androidx/media/issues/116)).
* Extractors:
* Add support for AVI
([#2092](https://github.com/google/ExoPlayer/issues/2092)).
* Fix parsing of H265 short term reference picture sets
([#10316](https://github.com/google/ExoPlayer/issues/10316)).
* Fix parsing of bitrates from `esds` boxes
([#10381](https://github.com/google/ExoPlayer/issues/10381)).
* DASH:
* Parse ClearKey license URL from manifests
([#10246](https://github.com/google/ExoPlayer/issues/10246)).
* UI:
* Ensure TalkBack announces the currently active speed option in the
playback controls menu
([#10298](https://github.com/google/ExoPlayer/issues/10298)).
* RTSP:
* Add RTP reader for H263
([#63](https://github.com/androidx/media/pull/63)).
* Add VP8 fragmented packet handling
([#110](https://github.com/androidx/media/pull/110)).
* Leanback extension:
* Listen to `playWhenReady` changes in `LeanbackAdapter`
([10420](https://github.com/google/ExoPlayer/issues/10420)).
* Cast:
* Use the `MediaItem` that has been passed to the playlist methods as
`Window.mediaItem` in `CastTimeline`
([#25](https://github.com/androidx/media/issues/25),
[#8212](https://github.com/google/ExoPlayer/issues/8212)).
* Support `Player.getMetadata()` and `Listener.onMediaMetadataChanged()`
with `CastPlayer` ([#25](https://github.com/androidx/media/issues/25)).
### 1.0.0-beta01 (2022-06-16)
@ -46,7 +80,9 @@ This release corresponds to the
* Rename `TracksInfo` to `Tracks` and `TracksInfo.TrackGroupInfo` to
`Tracks.Group`. `Player.getCurrentTracksInfo` and
`Player.Listener.onTracksInfoChanged` have also been renamed to
`Player.getCurrentTracks` and `Player.Listener.onTracksChanged`.
`Player.getCurrentTracks` and `Player.Listener.onTracksChanged`. This
includes 'un-deprecating' the `Player.Listener.onTracksChanged` method
name, but with different parameter types.
* Change `DefaultTrackSelector.buildUponParameters` and
`DefaultTrackSelector.Parameters.buildUpon` to return
`DefaultTrackSelector.Parameters.Builder` instead of the deprecated
@ -100,6 +136,8 @@ This release corresponds to the
* Remove `RawCcExtractor`, which was only used to handle a Google-internal
subtitle format.
* Extractors:
* Add support for AVI
([#2092](https://github.com/google/ExoPlayer/issues/2092)).
* Matroska: Parse `DiscardPadding` for Opus tracks.
* MP4: Parse bitrates from `esds` boxes.
* Ogg: Allow duplicate Opus ID and comment headers
@ -149,6 +187,8 @@ This release corresponds to the
of `DefaultCompositeSequenceableLoaderFactory` can be passed explicitly
if required.
* RTSP:
* Add RTP reader for H263
([#63](https://github.com/androidx/media/pull/63)).
* Add RTP reader for MPEG4
([#35](https://github.com/androidx/media/pull/35)).
* Add RTP reader for HEVC
@ -211,10 +251,11 @@ This release corresponds to the
AndroidStudio's gradle sync to fail
([#9933](https://github.com/google/ExoPlayer/issues/9933)).
* Remove deprecated symbols:
* Remove `Player.Listener.onTracksChanged`. Use
`Player.Listener.onTracksInfoChanged` instead.
* Remove `Player.Listener.onTracksChanged(TrackGroupArray,
TrackSelectionArray)`. Use `Player.Listener.onTracksChanged(Tracks)`
instead.
* Remove `Player.getCurrentTrackGroups` and
`Player.getCurrentTrackSelections`. Use `Player.getCurrentTracksInfo`
`Player.getCurrentTrackSelections`. Use `Player.getCurrentTracks`
instead. You can also continue to use `ExoPlayer.getCurrentTrackGroups`
and `ExoPlayer.getCurrentTrackSelections`, although these methods remain
deprecated.
@ -424,7 +465,7 @@ This release corresponds to the
when creating `PendingIntent`s
([#9528](https://github.com/google/ExoPlayer/issues/9528)).
* Remove deprecated symbols:
* Remove `Player.EventLister`. Use `Player.Listener` instead.
* Remove `Player.EventListener`. Use `Player.Listener` instead.
* Remove `MediaSourceFactory.setDrmSessionManager`,
`MediaSourceFactory.setDrmHttpDataSourceFactory`, and
`MediaSourceFactory.setDrmUserAgent`. Use

View File

@ -807,7 +807,7 @@ package androidx.media3.common {
field public static final int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1; // 0x1
}
@IntDef({androidx.media3.common.Player.COMMAND_INVALID, androidx.media3.common.Player.COMMAND_PLAY_PAUSE, androidx.media3.common.Player.COMMAND_PREPARE, androidx.media3.common.Player.COMMAND_STOP, androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION, androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT, androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_BACK, androidx.media3.common.Player.COMMAND_SEEK_FORWARD, androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH, androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE, androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE, androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_GET_TIMELINE, androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS, androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES, androidx.media3.common.Player.COMMAND_GET_VOLUME, androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VOLUME, androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE, androidx.media3.common.Player.COMMAND_GET_TEXT, androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS, androidx.media3.common.Player.COMMAND_GET_TRACKS, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.Command {
@IntDef({androidx.media3.common.Player.COMMAND_INVALID, androidx.media3.common.Player.COMMAND_PLAY_PAUSE, androidx.media3.common.Player.COMMAND_PREPARE, androidx.media3.common.Player.COMMAND_STOP, androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION, androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT, androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_BACK, androidx.media3.common.Player.COMMAND_SEEK_FORWARD, androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH, androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE, androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE, androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_GET_TIMELINE, androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS, androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES, androidx.media3.common.Player.COMMAND_GET_VOLUME, androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VOLUME, androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE, androidx.media3.common.Player.COMMAND_GET_TEXT, androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS, androidx.media3.common.Player.COMMAND_GET_TRACKS}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.Command {
}
public static final class Player.Commands {

View File

@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
project.ext {
releaseVersion = '1.0.0-beta01'
releaseVersionCode = 1_000_000_1_01
releaseVersion = '1.0.0-beta02'
releaseVersionCode = 1_000_000_1_02
minSdkVersion = 16
appTargetSdkVersion = 29
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some

View File

@ -78,6 +78,12 @@
<data android:scheme="file"/>
<data android:scheme="ssai"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.media3.demo.main.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.media3.demo.main.action.VIEW_LIST"/>
<category android:name="android.intent.category.DEFAULT"/>

View File

@ -27,6 +27,7 @@ import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.media3.common.MediaItem
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaBrowser
@ -164,7 +165,7 @@ class MainActivity : AppCompatActivity() {
val root: MediaItem = result.value!!
pushPathStack(root)
},
MoreExecutors.directExecutor()
ContextCompat.getMainExecutor(this)
)
}

View File

@ -29,6 +29,7 @@ import android.widget.LinearLayout
import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.session.MediaBrowser
@ -150,7 +151,7 @@ class PlayableFolderActivity : AppCompatActivity() {
val result = mediaItemFuture.get()!!
title.text = result.value!!.mediaMetadata.title
},
MoreExecutors.directExecutor()
ContextCompat.getMainExecutor(this)
)
childrenFuture.addListener(
{
@ -161,7 +162,7 @@ class PlayableFolderActivity : AppCompatActivity() {
subItemMediaList.addAll(children)
mediaListAdapter.notifyDataSetChanged()
},
MoreExecutors.directExecutor()
ContextCompat.getMainExecutor(this)
)
}

View File

@ -43,6 +43,12 @@
<data android:scheme="asset"/>
<data android:scheme="file"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.media3.demo.surface.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
</application>

View File

@ -49,6 +49,12 @@
<data android:scheme="asset"/>
<data android:scheme="file"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.media3.demo.transformer.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
<activity android:name=".TransformerActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"

386
github/media3-migration.sh Normal file
View File

@ -0,0 +1,386 @@
#!/bin/bash
# Copyright (C) 2022 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##
shopt -s extglob
PACKAGE_MAPPINGS='com.google.android.exoplayer2 androidx.media3.exoplayer
com.google.android.exoplayer2.analytics androidx.media3.exoplayer.analytics
com.google.android.exoplayer2.audio androidx.media3.exoplayer.audio
com.google.android.exoplayer2.castdemo androidx.media3.demo.cast
com.google.android.exoplayer2.database androidx.media3.database
com.google.android.exoplayer2.decoder androidx.media3.decoder
com.google.android.exoplayer2.demo androidx.media3.demo.main
com.google.android.exoplayer2.drm androidx.media3.exoplayer.drm
com.google.android.exoplayer2.ext.av1 androidx.media3.decoder.av1
com.google.android.exoplayer2.ext.cast androidx.media3.cast
com.google.android.exoplayer2.ext.cronet androidx.media3.datasource.cronet
com.google.android.exoplayer2.ext.ffmpeg androidx.media3.decoder.ffmpeg
com.google.android.exoplayer2.ext.flac androidx.media3.decoder.flac
com.google.android.exoplayer2.ext.ima androidx.media3.exoplayer.ima
com.google.android.exoplayer2.ext.leanback androidx.media3.ui.leanback
com.google.android.exoplayer2.ext.okhttp androidx.media3.datasource.okhttp
com.google.android.exoplayer2.ext.opus androidx.media3.decoder.opus
com.google.android.exoplayer2.ext.rtmp androidx.media3.datasource.rtmp
com.google.android.exoplayer2.ext.vp9 androidx.media3.decoder.vp9
com.google.android.exoplayer2.ext.workmanager androidx.media3.exoplayer.workmanager
com.google.android.exoplayer2.extractor androidx.media3.extractor
com.google.android.exoplayer2.gldemo androidx.media3.demo.gl
com.google.android.exoplayer2.mediacodec androidx.media3.exoplayer.mediacodec
com.google.android.exoplayer2.metadata androidx.media3.extractor.metadata
com.google.android.exoplayer2.offline androidx.media3.exoplayer.offline
com.google.android.exoplayer2.playbacktests androidx.media3.test.exoplayer.playback
com.google.android.exoplayer2.robolectric androidx.media3.test.utils.robolectric
com.google.android.exoplayer2.scheduler androidx.media3.exoplayer.scheduler
com.google.android.exoplayer2.source androidx.media3.exoplayer.source
com.google.android.exoplayer2.source.dash androidx.media3.exoplayer.dash
com.google.android.exoplayer2.source.hls androidx.media3.exoplayer.hls
com.google.android.exoplayer2.source.rtsp androidx.media3.exoplayer.rtsp
com.google.android.exoplayer2.source.smoothstreaming androidx.media3.exoplayer.smoothstreaming
com.google.android.exoplayer2.surfacedemo androidx.media3.demo.surface
com.google.android.exoplayer2.testdata androidx.media3.test.data
com.google.android.exoplayer2.testutil androidx.media3.test.utils
com.google.android.exoplayer2.text androidx.media3.extractor.text
com.google.android.exoplayer2.trackselection androidx.media3.exoplayer.trackselection
com.google.android.exoplayer2.transformer androidx.media3.transformer
com.google.android.exoplayer2.transformerdemo androidx.media3.demo.transformer
com.google.android.exoplayer2.ui androidx.media3.ui
com.google.android.exoplayer2.upstream androidx.media3.datasource
com.google.android.exoplayer2.upstream.cache androidx.media3.datasource.cache
com.google.android.exoplayer2.upstream.crypto androidx.media3.exoplayer.upstream.crypto
com.google.android.exoplayer2.util androidx.media3.common.util
com.google.android.exoplayer2.util androidx.media3.exoplayer.util
com.google.android.exoplayer2.video androidx.media3.exoplayer.video'
CLASS_RENAMINGS='com.google.android.exoplayer2.ui.StyledPlayerView androidx.media3.ui.PlayerView
StyledPlayerView PlayerView
com.google.android.exoplayer2.ui.StyledPlayerControlView androidx.media3.ui.PlayerControlView
StyledPlayerControlView PlayerControlView
com.google.android.exoplayer2.ExoPlayerLibraryInfo androidx.media3.common.MediaLibraryInfo
ExoPlayerLibraryInfo MediaLibraryInfo
com.google.android.exoplayer2.SimpleExoPlayer androidx.media3.exoplayer.ExoPlayer
SimpleExoPlayer ExoPlayer'
CLASS_MAPPINGS='com.google.android.exoplayer2.text.span androidx.media3.common.text HorizontalTextInVerticalContextSpan LanguageFeatureSpan RubySpan SpanUtil TextAnnotation TextEmphasisSpan
com.google.android.exoplayer2.text androidx.media3.common.text CueGroup Cue
com.google.android.exoplayer2.text androidx.media3.exoplayer.text ExoplayerCuesDecoder SubtitleDecoderFactory TextOutput TextRenderer
com.google.android.exoplayer2.upstream.crypto androidx.media3.datasource AesCipherDataSource AesCipherDataSink AesFlushingCipher
com.google.android.exoplayer2.util androidx.media3.common.util AtomicFile Assertions BundleableUtil BundleUtil Clock ClosedSource CodecSpecificDataUtil ColorParser ConditionVariable Consumer CopyOnWriteMultiset EGLSurfaceTexture GlProgram GlUtil HandlerWrapper LibraryLoader ListenerSet Log LongArray MediaFormatUtil NetworkTypeObserver NonNullApi NotificationUtil ParsableBitArray ParsableByteArray RepeatModeUtil RunnableFutureTask SystemClock SystemHandlerWrapper TimedValueQueue TimestampAdjuster TraceUtil UnknownNull UnstableApi UriUtil Util XmlPullParserUtil
com.google.android.exoplayer2.util androidx.media3.common ErrorMessageProvider FlagSet FileTypes MimeTypes PriorityTaskManager
com.google.android.exoplayer2.metadata androidx.media3.common Metadata
com.google.android.exoplayer2.metadata androidx.media3.exoplayer.metadata MetadataDecoderFactory MetadataOutput MetadataRenderer
com.google.android.exoplayer2.audio androidx.media3.common AudioAttributes AuxEffectInfo
com.google.android.exoplayer2.ui androidx.media3.common AdOverlayInfo AdViewProvider
com.google.android.exoplayer2.source.ads androidx.media3.common AdPlaybackState
com.google.android.exoplayer2.source androidx.media3.common MediaPeriodId TrackGroup
com.google.android.exoplayer2.offline androidx.media3.common StreamKey
com.google.android.exoplayer2.ui androidx.media3.exoplayer.offline DownloadNotificationHelper
com.google.android.exoplayer2.trackselection androidx.media3.common TrackSelectionParameters TrackSelectionOverride
com.google.android.exoplayer2.video androidx.media3.common ColorInfo VideoSize
com.google.android.exoplayer2.upstream androidx.media3.common DataReader
com.google.android.exoplayer2.upstream androidx.media3.exoplayer.upstream Allocation Allocator BandwidthMeter CachedRegionTracker DefaultAllocator DefaultBandwidthMeter DefaultLoadErrorHandlingPolicy Loader LoaderErrorThrower ParsingLoadable SlidingPercentile TimeToFirstByteEstimator
com.google.android.exoplayer2.audio androidx.media3.extractor AacUtil Ac3Util Ac4Util DtsUtil MpegAudioUtil OpusUtil WavUtil
com.google.android.exoplayer2.util androidx.media3.extractor NalUnitUtil ParsableNalUnitBitArray
com.google.android.exoplayer2.video androidx.media3.extractor AvcConfig DolbyVisionConfig HevcConfig
com.google.android.exoplayer2.decoder androidx.media3.exoplayer DecoderCounters DecoderReuseEvaluation
com.google.android.exoplayer2.util androidx.media3.exoplayer MediaClock StandaloneMediaClock
com.google.android.exoplayer2 androidx.media3.exoplayer FormatHolder PlayerMessage
com.google.android.exoplayer2 androidx.media3.common BasePlayer BundleListRetriever Bundleable ControlDispatcher C DefaultControlDispatcher DeviceInfo ErrorMessageProvider ExoPlayerLibraryInfo Format ForwardingPlayer HeartRating IllegalSeekPositionException MediaItem MediaMetadata ParserException PercentageRating PlaybackException PlaybackParameters Player PositionInfo Rating StarRating ThumbRating Timeline Tracks
com.google.android.exoplayer2.drm androidx.media3.common DrmInitData'
DEPENDENCY_MAPPINGS='exoplayer media3-exoplayer
exoplayer-common media3-common
exoplayer-core media3-exoplayer
exoplayer-dash media3-exoplayer-dash
exoplayer-database media3-database
exoplayer-datasource media-datasource
exoplayer-decoder media3-decoder
exoplayer-extractor media3-extractor
exoplayer-hls media3-exoplayer-hls
exoplayer-robolectricutils media3-test-utils-robolectric
exoplayer-rtsp media3-exoplayer-rtsp
exoplayer-smoothstreaming media3-exoplayer-smoothstreaming
exoplayer-testutils media3-test-utils
exoplayer-transformer media3-transformer
exoplayer-ui media3-ui
extension-cast media3-cast
extension-cronet media3-datasource-cronet
extension-ima media3-exoplayer-ima
extension-leanback media3-ui-leanback
extension-okhttp media3-datasource-okhttp
extension-rtmp media3-datasource-rtmp
extension-workmanager media3-exoplayer-workmanager'
# Rewrites classes, packages and dependencies from the legacy ExoPlayer package structure
# to androidx.media3 structure.
MEDIA3_VERSION="1.0.0-beta02"
LEGACY_PEER_VERSION="2.18.1"
function usage() {
echo "usage: $0 [-p|-c|-d|-v]|[-m|-l [-x <path>] [-f] PROJECT_ROOT]"
echo " PROJECT_ROOT: path to your project root (location of 'gradlew')"
echo " -p: list package mappings and then exit"
echo " -c: list class mappings (precedence over package mappings) and then exit"
echo " -d: list dependency mappings and then exit"
echo " -m: migrate packages, classes and dependencies to AndroidX Media3"
echo " -l: list files that will be considered for rewrite and then exit"
echo " -x: exclude the path from the list of file to be changed: 'app/src/test'"
echo " -f: force the action even when validation fails"
echo " -v: print the exoplayer2/media3 version strings of this script and exit"
echo " --noclean : Do not call './gradlew clean' in project directory."
echo " -h, --help: show this help text"
}
function print_pairs {
while read -r line;
do
IFS=' ' read -ra PAIR <<< "$line"
printf "%-55s %-30s\n" "${PAIR[0]}" "${PAIR[1]}"
done <<< "$(echo "$@")"
}
function print_class_mappings {
while read -r mapping;
do
old=$(echo "$mapping" | cut -d ' ' -f1)
new=$(echo "$mapping" | cut -d ' ' -f2)
classes=$(echo "$mapping" | cut -d ' ' -f3-)
for clazz in $classes;
do
printf "%-80s %-30s\n" "$old.$clazz" "$new.$clazz"
done
done <<< "$(echo "$CLASS_MAPPINGS" | sort)"
}
ERROR_COUNTER=0
VALIDATION_ERRORS=''
function add_validation_error {
let ERROR_COUNTER++
VALIDATION_ERRORS+="\033[31m[$ERROR_COUNTER] ->\033[0m ${1}"
}
function validate_exoplayer_version() {
has_exoplayer_dependency=''
while read -r file;
do
local version
version=$(grep -m 1 "com\.google\.android\.exoplayer:" "$file" | cut -d ":" -f3 | tr -d \" | tr -d \')
if [[ ! -z $version ]] && [[ ! "$version" =~ $LEGACY_PEER_VERSION ]];
then
add_validation_error "The version does not match '$LEGACY_PEER_VERSION'. \
Update to '$LEGACY_PEER_VERSION' or use the migration script matching your \
current version. Current version '$version' found in\n $file\n"
fi
done <<< "$(find . -type f -name "build.gradle")"
}
function validate_string_not_contained {
local pattern=$1 # regex
local failure_message=$2
while read -r file;
do
if grep -q -e "$pattern" "$file";
then
add_validation_error "$failure_message:\n $file\n"
fi
done <<< "$files"
}
function validate_string_patterns {
validate_string_not_contained \
'com\.google\.android\.exoplayer2\..*\*' \
'Replace wildcard import statements with fully qualified import statements';
validate_string_not_contained \
'com\.google\.android\.exoplayer2\.ui\.PlayerView' \
'Migrate PlayerView to StyledPlayerView before migrating';
validate_string_not_contained \
'LegacyPlayerView' \
'Migrate LegacyPlayerView to StyledPlayerView before migrating';
validate_string_not_contained \
'com\.google\.android\.exoplayer2\.ext\.mediasession' \
'The MediaSessionConnector is integrated in androidx.media3.session.MediaSession'
}
SED_CMD_INPLACE='sed -i '
if [[ "$OSTYPE" == "darwin"* ]]; then
SED_CMD_INPLACE="sed -i '' "
fi
MIGRATE_FILES='1'
LIST_FILES_ONLY='1'
PRINT_CLASS_MAPPINGS='1'
PRINT_PACKAGE_MAPPINGS='1'
PRINT_DEPENDENCY_MAPPINGS='1'
PRINT_VERSION='1'
NO_CLEAN='1'
FORCE='1'
IGNORE_VERSION='1'
EXCLUDED_PATHS=''
while [[ $1 =~ ^-.* ]];
do
case "$1" in
-m ) MIGRATE_FILES='';;
-l ) LIST_FILES_ONLY='';;
-c ) PRINT_CLASS_MAPPINGS='';;
-p ) PRINT_PACKAGE_MAPPINGS='';;
-d ) PRINT_DEPENDENCY_MAPPINGS='';;
-v ) PRINT_VERSION='';;
-f ) FORCE='';;
-x ) shift; EXCLUDED_PATHS="$(printf "%s\n%s" $EXCLUDED_PATHS $1)";;
--noclean ) NO_CLEAN='';;
* ) usage && exit 1;;
esac
shift
done
if [[ -z $PRINT_DEPENDENCY_MAPPINGS ]];
then
print_pairs "$DEPENDENCY_MAPPINGS"
exit 0
elif [[ -z $PRINT_PACKAGE_MAPPINGS ]];
then
print_pairs "$PACKAGE_MAPPINGS"
exit 0
elif [[ -z $PRINT_CLASS_MAPPINGS ]];
then
print_class_mappings
exit 0
elif [[ -z $PRINT_VERSION ]];
then
echo "$LEGACY_PEER_VERSION -> $MEDIA3_VERSION. This script is written to migrate from ExoPlayer $LEGACY_PEER_VERSION to AndroidX Media3 $MEDIA3_VERSION"
exit 0
elif [[ -z $1 ]];
then
usage
exit 1
fi
if [[ ! -f $1/gradlew ]];
then
echo "directory seems not to exist or is not a gradle project (missing 'gradlew')"
usage
exit 1
fi
PROJECT_ROOT=$1
cd "$PROJECT_ROOT"
# Create the set of files to transform
exclusion="/build/|/.idea/|/res/drawable|/res/color|/res/mipmap|/res/values|"
if [[ ! -z $EXCLUDED_PATHS ]];
then
while read -r path;
do
exclusion="$exclusion./$path|"
done <<< "$EXCLUDED_PATHS"
fi
files=$(find . -name '*\.java' -o -name '*\.kt' -o -name '*\.xml' | grep -Ev "'$exclusion'")
# Validate project and exit in case of validation errors
validate_string_patterns
validate_exoplayer_version "$PROJECT_ROOT"
if [[ ! -z $FORCE && ! -z "$VALIDATION_ERRORS" ]];
then
echo "============================================="
echo "Validation errors (use -f to force execution)"
echo "---------------------------------------------"
echo -e "$VALIDATION_ERRORS"
exit 1
fi
if [[ -z $LIST_FILES_ONLY ]];
then
echo "$files" | cut -c 3-
find . -type f -name 'build\.gradle' | cut -c 3-
exit 0
fi
# start migration after successful validation or when forced to disregard validation
# errors
if [[ ! -z "$MIGRATE_FILES" ]];
then
echo "nothing to do"
usage
exit 0
fi
PWD=$(pwd)
if [[ ! -z $NO_CLEAN ]];
then
cd "$PROJECT_ROOT"
./gradlew clean
cd "$PWD"
fi
# create expressions for class renamings
renaming_expressions=''
while read -r renaming;
do
src=$(echo "$renaming" | cut -d ' ' -f1 | sed -e 's/\./\\\./g')
dest=$(echo "$renaming" | cut -d ' ' -f2)
renaming_expressions+="-e s/$src/$dest/g "
done <<< "$CLASS_RENAMINGS"
# create expressions for class mappings
classes_expressions=''
while read -r mapping;
do
src=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g')
dest=$(echo "$mapping" | cut -d ' ' -f2)
classes=$(echo "$mapping" | cut -d ' ' -f3-)
for clazz in $classes;
do
classes_expressions+="-e s/$src\.$clazz/$dest.$clazz/g "
done
done <<< "$CLASS_MAPPINGS"
# create expressions for package mappings
packages_expressions=''
while read -r mapping;
do
src=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g')
dest=$(echo "$mapping" | cut -d ' ' -f2)
packages_expressions+="-e s/$src/$dest/g "
done <<< "$PACKAGE_MAPPINGS"
# do search and replace with expressions in each selected file
while read -r file;
do
echo "migrating $file"
expr="$renaming_expressions $classes_expressions $packages_expressions"
$SED_CMD_INPLACE $expr $file
done <<< "$files"
# create expressions for dependencies in gradle files
EXOPLAYER_GROUP="com\.google\.android\.exoplayer"
MEDIA3_GROUP="androidx.media3"
dependency_expressions=""
while read -r mapping
do
OLD=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g')
NEW=$(echo "$mapping" | cut -d ' ' -f2)
dependency_expressions="$dependency_expressions -e s/$EXOPLAYER_GROUP:$OLD:.*\"/$MEDIA3_GROUP:$NEW:$MEDIA3_VERSION\"/g -e s/$EXOPLAYER_GROUP:$OLD:.*'/$MEDIA3_GROUP:$NEW:$MEDIA3_VERSION'/"
done <<< "$DEPENDENCY_MAPPINGS"
## do search and replace for dependencies in gradle files
while read -r build_file;
do
echo "migrating build file $build_file"
$SED_CMD_INPLACE $dependency_expressions $build_file
done <<< "$(find . -type f -name 'build\.gradle')"

View File

@ -101,9 +101,9 @@ public final class CastPlayer extends BasePlayer {
COMMAND_GET_TIMELINE,
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEM,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_TRACKS,
COMMAND_SET_MEDIA_ITEM)
COMMAND_GET_TRACKS)
.build();
public static final float MIN_SPEED_SUPPORTED = 0.5f;
@ -145,6 +145,7 @@ public final class CastPlayer extends BasePlayer {
private int pendingSeekWindowIndex;
private long pendingSeekPositionMs;
@Nullable private PositionInfo pendingMediaItemRemovalPosition;
private MediaMetadata mediaMetadata;
/**
* Creates a new cast player.
@ -198,7 +199,7 @@ public final class CastPlayer extends BasePlayer {
this.mediaItemConverter = mediaItemConverter;
this.seekBackIncrementMs = seekBackIncrementMs;
this.seekForwardIncrementMs = seekForwardIncrementMs;
timelineTracker = new CastTimelineTracker();
timelineTracker = new CastTimelineTracker(mediaItemConverter);
period = new Timeline.Period();
statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback();
@ -212,6 +213,7 @@ public final class CastPlayer extends BasePlayer {
playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT);
playbackState = STATE_IDLE;
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
mediaMetadata = MediaMetadata.EMPTY;
currentTracks = Tracks.EMPTY;
availableCommands = new Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build();
pendingSeekWindowIndex = C.INDEX_UNSET;
@ -283,8 +285,7 @@ public final class CastPlayer extends BasePlayer {
@Override
public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
setMediaItemsInternal(
toMediaQueueItems(mediaItems), startIndex, startPositionMs, repeatMode.value);
setMediaItemsInternal(mediaItems, startIndex, startPositionMs, repeatMode.value);
}
@Override
@ -294,7 +295,7 @@ public final class CastPlayer extends BasePlayer {
if (index < currentTimeline.getWindowCount()) {
uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid;
}
addMediaItemsInternal(toMediaQueueItems(mediaItems), uid);
addMediaItemsInternal(mediaItems, uid);
}
@Override
@ -426,6 +427,13 @@ public final class CastPlayer extends BasePlayer {
Player.EVENT_MEDIA_ITEM_TRANSITION,
listener ->
listener.onMediaItemTransition(mediaItem, MEDIA_ITEM_TRANSITION_REASON_SEEK));
MediaMetadata oldMediaMetadata = mediaMetadata;
mediaMetadata = getMediaMetadataInternal();
if (!oldMediaMetadata.equals(mediaMetadata)) {
listeners.queueEvent(
Player.EVENT_MEDIA_METADATA_CHANGED,
listener -> listener.onMediaMetadataChanged(mediaMetadata));
}
}
updateAvailableCommandsAndNotifyIfChanged();
} else if (pendingSeekCount == 0) {
@ -563,8 +571,12 @@ public final class CastPlayer extends BasePlayer {
@Override
public MediaMetadata getMediaMetadata() {
// CastPlayer does not currently support metadata.
return MediaMetadata.EMPTY;
return mediaMetadata;
}
public MediaMetadata getMediaMetadataInternal() {
MediaItem currentMediaItem = getCurrentMediaItem();
return currentMediaItem != null ? currentMediaItem.mediaMetadata : MediaMetadata.EMPTY;
}
@Override
@ -761,6 +773,7 @@ public final class CastPlayer extends BasePlayer {
return;
}
int oldWindowIndex = this.currentWindowIndex;
MediaMetadata oldMediaMetadata = mediaMetadata;
@Nullable
Object oldPeriodUid =
!getCurrentTimeline().isEmpty()
@ -772,6 +785,7 @@ public final class CastPlayer extends BasePlayer {
boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged();
Timeline currentTimeline = getCurrentTimeline();
currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline);
mediaMetadata = getMediaMetadataInternal();
@Nullable
Object currentPeriodUid =
!currentTimeline.isEmpty()
@ -825,6 +839,11 @@ public final class CastPlayer extends BasePlayer {
listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(currentTracks));
}
if (!oldMediaMetadata.equals(mediaMetadata)) {
listeners.queueEvent(
Player.EVENT_MEDIA_METADATA_CHANGED,
listener -> listener.onMediaMetadataChanged(mediaMetadata));
}
updateAvailableCommandsAndNotifyIfChanged();
listeners.flushEvents();
}
@ -1020,14 +1039,13 @@ public final class CastPlayer extends BasePlayer {
}
}
@Nullable
private PendingResult<MediaChannelResult> setMediaItemsInternal(
MediaQueueItem[] mediaQueueItems,
private void setMediaItemsInternal(
List<MediaItem> mediaItems,
int startIndex,
long startPositionMs,
@RepeatMode int repeatMode) {
if (remoteMediaClient == null || mediaQueueItems.length == 0) {
return null;
if (remoteMediaClient == null || mediaItems.isEmpty()) {
return;
}
startPositionMs = startPositionMs == C.TIME_UNSET ? 0 : startPositionMs;
if (startIndex == C.INDEX_UNSET) {
@ -1038,34 +1056,35 @@ public final class CastPlayer extends BasePlayer {
if (!currentTimeline.isEmpty()) {
pendingMediaItemRemovalPosition = getCurrentPositionInfo();
}
return remoteMediaClient.queueLoad(
MediaQueueItem[] mediaQueueItems = toMediaQueueItems(mediaItems);
timelineTracker.onMediaItemsSet(mediaItems, mediaQueueItems);
remoteMediaClient.queueLoad(
mediaQueueItems,
min(startIndex, mediaQueueItems.length - 1),
min(startIndex, mediaItems.size() - 1),
getCastRepeatMode(repeatMode),
startPositionMs,
/* customData= */ null);
}
@Nullable
private PendingResult<MediaChannelResult> addMediaItemsInternal(MediaQueueItem[] items, int uid) {
private void addMediaItemsInternal(List<MediaItem> mediaItems, int uid) {
if (remoteMediaClient == null || getMediaStatus() == null) {
return null;
return;
}
return remoteMediaClient.queueInsertItems(items, uid, /* customData= */ null);
MediaQueueItem[] itemsToInsert = toMediaQueueItems(mediaItems);
timelineTracker.onMediaItemsAdded(mediaItems, itemsToInsert);
remoteMediaClient.queueInsertItems(itemsToInsert, uid, /* customData= */ null);
}
@Nullable
private PendingResult<MediaChannelResult> moveMediaItemsInternal(
int[] uids, int fromIndex, int newIndex) {
private void moveMediaItemsInternal(int[] uids, int fromIndex, int newIndex) {
if (remoteMediaClient == null || getMediaStatus() == null) {
return null;
return;
}
int insertBeforeIndex = fromIndex < newIndex ? newIndex + uids.length : newIndex;
int insertBeforeItemId = MediaQueueItem.INVALID_ITEM_ID;
if (insertBeforeIndex < currentTimeline.getWindowCount()) {
insertBeforeItemId = (int) currentTimeline.getWindow(insertBeforeIndex, window).uid;
}
return remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null);
remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null);
}
@Nullable

View File

@ -15,13 +15,13 @@
*/
package androidx.media3.cast;
import android.net.Uri;
import android.util.SparseArray;
import android.util.SparseIntArray;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import com.google.android.gms.cast.MediaInfo;
import java.util.Arrays;
/** A {@link Timeline} for Cast media queues. */
@ -30,12 +30,16 @@ import java.util.Arrays;
/** Holds {@link Timeline} related data for a Cast media item. */
public static final class ItemData {
/* package */ static final String UNKNOWN_CONTENT_ID = "UNKNOWN_CONTENT_ID";
/** Holds no media information. */
public static final ItemData EMPTY =
new ItemData(
/* durationUs= */ C.TIME_UNSET,
/* defaultPositionUs= */ C.TIME_UNSET,
/* isLive= */ false);
/* isLive= */ false,
MediaItem.EMPTY,
UNKNOWN_CONTENT_ID);
/** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
public final long durationUs;
@ -45,6 +49,10 @@ import java.util.Arrays;
public final long defaultPositionUs;
/** Whether the item is live content, or {@code false} if unknown. */
public final boolean isLive;
/** The original media item that has been set or added to the playlist. */
public final MediaItem mediaItem;
/** The {@linkplain MediaInfo#getContentId() content ID} of the cast media queue item. */
public final String contentId;
/**
* Creates an instance.
@ -52,11 +60,20 @@ import java.util.Arrays;
* @param durationUs See {@link #durationsUs}.
* @param defaultPositionUs See {@link #defaultPositionUs}.
* @param isLive See {@link #isLive}.
* @param mediaItem See {@link #mediaItem}.
* @param contentId See {@link #contentId}.
*/
public ItemData(long durationUs, long defaultPositionUs, boolean isLive) {
public ItemData(
long durationUs,
long defaultPositionUs,
boolean isLive,
MediaItem mediaItem,
String contentId) {
this.durationUs = durationUs;
this.defaultPositionUs = defaultPositionUs;
this.isLive = isLive;
this.mediaItem = mediaItem;
this.contentId = contentId;
}
/**
@ -66,14 +83,23 @@ import java.util.Arrays;
* @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET}
* if unknown.
* @param isLive Whether the item is live, or {@code false} if unknown.
* @param mediaItem The media item.
* @param contentId The content ID.
*/
public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boolean isLive) {
public ItemData copyWithNewValues(
long durationUs,
long defaultPositionUs,
boolean isLive,
MediaItem mediaItem,
String contentId) {
if (durationUs == this.durationUs
&& defaultPositionUs == this.defaultPositionUs
&& isLive == this.isLive) {
&& isLive == this.isLive
&& contentId.equals(this.contentId)
&& mediaItem.equals(this.mediaItem)) {
return this;
}
return new ItemData(durationUs, defaultPositionUs, isLive);
return new ItemData(durationUs, defaultPositionUs, isLive, mediaItem, contentId);
}
}
@ -82,6 +108,7 @@ import java.util.Arrays;
new CastTimeline(new int[0], new SparseArray<>());
private final SparseIntArray idsToIndex;
private final MediaItem[] mediaItems;
private final int[] ids;
private final long[] durationsUs;
private final long[] defaultPositionsUs;
@ -100,10 +127,12 @@ import java.util.Arrays;
durationsUs = new long[itemCount];
defaultPositionsUs = new long[itemCount];
isLive = new boolean[itemCount];
mediaItems = new MediaItem[itemCount];
for (int i = 0; i < ids.length; i++) {
int id = ids[i];
idsToIndex.put(id, i);
ItemData data = itemIdToData.get(id, ItemData.EMPTY);
mediaItems[i] = data.mediaItem;
durationsUs[i] = data.durationUs;
defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs;
isLive[i] = data.isLive;
@ -121,18 +150,16 @@ import java.util.Arrays;
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET;
MediaItem mediaItem =
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build();
return window.set(
/* uid= */ ids[windowIndex],
/* mediaItem= */ mediaItem,
/* mediaItem= */ mediaItems[windowIndex],
/* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic,
isDynamic,
isLive[windowIndex] ? mediaItem.liveConfiguration : null,
isLive[windowIndex] ? mediaItems[windowIndex].liveConfiguration : null,
defaultPositionsUs[windowIndex],
durationUs,
/* firstPeriodIndex= */ windowIndex,

View File

@ -15,14 +15,23 @@
*/
package androidx.media3.cast;
import static androidx.media3.cast.CastTimeline.ItemData.UNKNOWN_CONTENT_ID;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/**
* Creates {@link CastTimeline CastTimelines} from cast receiver app status updates.
@ -33,9 +42,47 @@ import java.util.HashSet;
/* package */ final class CastTimelineTracker {
private final SparseArray<CastTimeline.ItemData> itemIdToData;
private final MediaItemConverter mediaItemConverter;
@VisibleForTesting /* package */ final HashMap<String, MediaItem> mediaItemsByContentId;
public CastTimelineTracker() {
/**
* Creates an instance.
*
* @param mediaItemConverter The converter used to convert from a {@link MediaQueueItem} to a
* {@link MediaItem}.
*/
public CastTimelineTracker(MediaItemConverter mediaItemConverter) {
this.mediaItemConverter = mediaItemConverter;
itemIdToData = new SparseArray<>();
mediaItemsByContentId = new HashMap<>();
}
/**
* Called when media items {@linkplain Player#setMediaItems have been set to the playlist} and are
* sent to the cast playback queue. A future queue update of the {@link RemoteMediaClient} will
* reflect this addition.
*
* @param mediaItems The media items that have been set.
* @param mediaQueueItems The corresponding media queue items.
*/
public void onMediaItemsSet(List<MediaItem> mediaItems, MediaQueueItem[] mediaQueueItems) {
mediaItemsByContentId.clear();
onMediaItemsAdded(mediaItems, mediaQueueItems);
}
/**
* Called when media items {@linkplain Player#addMediaItems(List) have been added} and are sent to
* the cast playback queue. A future queue update of the {@link RemoteMediaClient} will reflect
* this addition.
*
* @param mediaItems The media items that have been added.
* @param mediaQueueItems The corresponding media queue items.
*/
public void onMediaItemsAdded(List<MediaItem> mediaItems, MediaQueueItem[] mediaQueueItems) {
for (int i = 0; i < mediaItems.size(); i++) {
mediaItemsByContentId.put(
checkNotNull(mediaQueueItems[i].getMedia()).getContentId(), mediaItems.get(i));
}
}
/**
@ -63,18 +110,36 @@ import java.util.HashSet;
}
int currentItemId = mediaStatus.getCurrentItemId();
String currentContentId = checkStateNotNull(mediaStatus.getMediaInfo()).getContentId();
MediaItem mediaItem = mediaItemsByContentId.get(currentContentId);
updateItemData(
currentItemId, mediaStatus.getMediaInfo(), /* defaultPositionUs= */ C.TIME_UNSET);
currentItemId,
mediaItem != null ? mediaItem : MediaItem.EMPTY,
mediaStatus.getMediaInfo(),
currentContentId,
/* defaultPositionUs= */ C.TIME_UNSET);
for (MediaQueueItem item : mediaStatus.getQueueItems()) {
long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs);
for (MediaQueueItem queueItem : mediaStatus.getQueueItems()) {
long defaultPositionUs = (long) (queueItem.getStartTime() * C.MICROS_PER_SECOND);
@Nullable MediaInfo mediaInfo = queueItem.getMedia();
String contentId = mediaInfo != null ? mediaInfo.getContentId() : UNKNOWN_CONTENT_ID;
mediaItem = mediaItemsByContentId.get(contentId);
updateItemData(
queueItem.getItemId(),
mediaItem != null ? mediaItem : mediaItemConverter.toMediaItem(queueItem),
mediaInfo,
contentId,
defaultPositionUs);
}
return new CastTimeline(itemIds, itemIdToData);
}
private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs) {
private void updateItemData(
int itemId,
MediaItem mediaItem,
@Nullable MediaInfo mediaInfo,
String contentId,
long defaultPositionUs) {
CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY);
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
if (durationUs == C.TIME_UNSET) {
@ -87,7 +152,10 @@ import java.util.HashSet;
if (defaultPositionUs == C.TIME_UNSET) {
defaultPositionUs = previousData.defaultPositionUs;
}
itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive));
itemIdToData.put(
itemId,
previousData.copyWithNewValues(
durationUs, defaultPositionUs, isLive, mediaItem, contentId));
}
private void removeUnusedItemDataEntries(int[] itemIds) {
@ -99,6 +167,8 @@ import java.util.HashSet;
int index = 0;
while (index < itemIdToData.size()) {
if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {
CastTimeline.ItemData itemData = itemIdToData.valueAt(index);
mediaItemsByContentId.remove(itemData.contentId);
itemIdToData.removeAt(index);
} else {
index++;

View File

@ -128,11 +128,14 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
if (mediaItem.mediaMetadata.trackNumber != null) {
metadata.putInt(MediaMetadata.KEY_TRACK_NUMBER, mediaItem.mediaMetadata.trackNumber);
}
String contentUrl = mediaItem.localConfiguration.uri.toString();
String contentId =
mediaItem.mediaId.equals(MediaItem.DEFAULT_MEDIA_ID) ? contentUrl : mediaItem.mediaId;
MediaInfo mediaInfo =
new MediaInfo.Builder(mediaItem.localConfiguration.uri.toString())
new MediaInfo.Builder(contentId)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(mediaItem.localConfiguration.mimeType)
.setContentUrl(contentUrl)
.setMetadata(metadata)
.setCustomData(getCustomData(mediaItem))
.build();

View File

@ -63,9 +63,11 @@ import static org.mockito.MockitoAnnotations.initMocks;
import android.net.Uri;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.Player.Listener;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -98,6 +100,7 @@ import org.mockito.Mockito;
public class CastPlayerTest {
private CastPlayer castPlayer;
private DefaultMediaItemConverter mediaItemConverter;
private RemoteMediaClient.Callback remoteMediaClientCallback;
@Mock private RemoteMediaClient mockRemoteMediaClient;
@ -106,7 +109,7 @@ public class CastPlayerTest {
@Mock private CastContext mockCastContext;
@Mock private SessionManager mockSessionManager;
@Mock private CastSession mockCastSession;
@Mock private Player.Listener mockListener;
@Mock private Listener mockListener;
@Mock private PendingResult<RemoteMediaClient.MediaChannelResult> mockPendingResult;
@Captor
@ -126,12 +129,14 @@ public class CastPlayerTest {
when(mockCastSession.getRemoteMediaClient()).thenReturn(mockRemoteMediaClient);
when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus);
when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue);
when(mockMediaStatus.getMediaInfo()).thenReturn(new MediaInfo.Builder("contentId").build());
when(mockMediaQueue.getItemIds()).thenReturn(new int[0]);
// Make the remote media client present the same default values as ExoPlayer:
when(mockRemoteMediaClient.isPaused()).thenReturn(true);
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
when(mockMediaStatus.getPlaybackRate()).thenReturn(1.0d);
castPlayer = new CastPlayer(mockCastContext);
mediaItemConverter = new DefaultMediaItemConverter();
castPlayer = new CastPlayer(mockCastContext, mediaItemConverter);
castPlayer.addListener(mockListener);
verify(mockRemoteMediaClient).registerCallback(callbackArgumentCaptor.capture());
remoteMediaClientCallback = callbackArgumentCaptor.getValue();
@ -388,7 +393,7 @@ public class CastPlayerTest {
mediaItems.add(
new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build());
castPlayer.setMediaItems(mediaItems, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L);
castPlayer.setMediaItems(mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 2000L);
verify(mockRemoteMediaClient)
.queueLoad(queueItemsArgumentCaptor.capture(), eq(1), anyInt(), eq(2000L), any());
@ -434,22 +439,23 @@ public class CastPlayerTest {
.setMimeType(MimeTypes.APPLICATION_MPD)
.build());
castPlayer.setMediaItems(
firstPlaylist, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L);
castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 1, /* startPositionMs= */ 2000L);
updateTimeLine(
firstPlaylist, /* mediaQueueItemIds= */ new int[] {1, 2}, /* currentItemId= */ 2);
// Replacing existing playlist.
castPlayer.setMediaItems(
secondPlaylist, /* startWindowIndex= */ 0, /* startPositionMs= */ 1000L);
castPlayer.setMediaItems(secondPlaylist, /* startIndex= */ 0, /* startPositionMs= */ 1000L);
updateTimeLine(secondPlaylist, /* mediaQueueItemIds= */ new int[] {3}, /* currentItemId= */ 3);
InOrder inOrder = Mockito.inOrder(mockListener);
inOrder
.verify(mockListener, times(2))
.verify(mockListener)
.onMediaItemTransition(
mediaItemCaptor.capture(), eq(MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
eq(firstPlaylist.get(1)), eq(MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder
.verify(mockListener)
.onMediaItemTransition(
eq(secondPlaylist.get(0)), eq(MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
assertThat(mediaItemCaptor.getAllValues().get(1).localConfiguration.tag).isEqualTo(3);
}
@SuppressWarnings("deprecation") // Verifies deprecated callback being called correctly.
@ -469,8 +475,7 @@ public class CastPlayerTest {
.setMimeType(MimeTypes.APPLICATION_MPD)
.build());
castPlayer.setMediaItems(
firstPlaylist, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L);
castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 1, /* startPositionMs= */ 2000L);
updateTimeLine(
firstPlaylist,
/* mediaQueueItemIds= */ new int[] {1, 2},
@ -481,8 +486,7 @@ public class CastPlayerTest {
/* durationsMs= */ new long[] {20_000, 20_000},
/* positionMs= */ 2000L);
// Replacing existing playlist.
castPlayer.setMediaItems(
secondPlaylist, /* startWindowIndex= */ 0, /* startPositionMs= */ 1000L);
castPlayer.setMediaItems(secondPlaylist, /* startIndex= */ 0, /* startPositionMs= */ 1000L);
updateTimeLine(
secondPlaylist,
/* mediaQueueItemIds= */ new int[] {3},
@ -494,8 +498,8 @@ public class CastPlayerTest {
Player.PositionInfo oldPosition =
new Player.PositionInfo(
/* windowUid= */ 2,
/* windowIndex= */ 1,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(),
/* mediaItemIndex= */ 1,
firstPlaylist.get(1),
/* periodUid= */ 2,
/* periodIndex= */ 1,
/* positionMs= */ 2000,
@ -505,8 +509,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition =
new Player.PositionInfo(
/* windowUid= */ 3,
/* windowIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(3).build(),
/* mediaItemIndex= */ 0,
secondPlaylist.get(0),
/* periodUid= */ 3,
/* periodIndex= */ 0,
/* positionMs= */ 1000,
@ -536,34 +540,37 @@ public class CastPlayerTest {
verify(mockRemoteMediaClient)
.queueInsertItems(
queueItemsArgumentCaptor.capture(), eq(MediaQueueItem.INVALID_ITEM_ID), any());
MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue();
assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(uri1);
assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(uri2);
}
@SuppressWarnings("ConstantConditions")
@Test
public void addMediaItems_insertAtIndex_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 2);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
// Add two items.
addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
String uri = "http://www.google.com/video3";
MediaItem anotherMediaItem =
new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_MPD).build();
int index = 1;
List<MediaItem> newPlaylist = Collections.singletonList(anotherMediaItem);
// Add another on position 1
int index = 1;
castPlayer.addMediaItems(index, Collections.singletonList(anotherMediaItem));
castPlayer.addMediaItems(index, newPlaylist);
updateTimeLine(newPlaylist, /* mediaQueueItemIds= */ new int[] {123}, /* currentItemId= */ 1);
verify(mockRemoteMediaClient)
.queueInsertItems(
queueItemsArgumentCaptor.capture(),
eq((int) mediaItems.get(index).localConfiguration.tag),
any());
MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue();
assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(uri);
verify(mockRemoteMediaClient, times(2))
.queueInsertItems(queueItemsArgumentCaptor.capture(), anyInt(), any());
assertThat(queueItemsArgumentCaptor.getAllValues().get(1)[0])
.isEqualTo(mediaItemConverter.toMediaQueueItem(anotherMediaItem));
Timeline.Window currentWindow =
castPlayer
.getCurrentTimeline()
.getWindow(castPlayer.getCurrentMediaItemIndex(), new Timeline.Window());
assertThat(currentWindow.uid).isEqualTo(123);
assertThat(currentWindow.mediaItem).isEqualTo(anotherMediaItem);
}
@Test
@ -702,8 +709,8 @@ public class CastPlayerTest {
Timeline currentTimeline = castPlayer.getCurrentTimeline();
for (int i = 0; i < mediaItems.size(); i++) {
assertThat(currentTimeline.getWindow(/* windowIndex= */ i, window).uid)
.isEqualTo(mediaItems.get(i).localConfiguration.tag);
assertThat(currentTimeline.getWindow(/* windowIndex= */ i, window).mediaItem)
.isEqualTo(mediaItems.get(i));
}
}
@ -720,10 +727,8 @@ public class CastPlayerTest {
inOrder
.verify(mockListener)
.onMediaItemTransition(
mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
eq(mediaItem), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
assertThat(mediaItemCaptor.getValue().localConfiguration.tag)
.isEqualTo(mediaItem.localConfiguration.tag);
}
@Test
@ -742,7 +747,8 @@ public class CastPlayerTest {
InOrder inOrder = Mockito.inOrder(mockListener);
inOrder
.verify(mockListener)
.onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
.onMediaItemTransition(
eq(mediaItems.get(0)), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder
.verify(mockListener)
.onMediaItemTransition(
@ -776,8 +782,8 @@ public class CastPlayerTest {
Player.PositionInfo oldPosition =
new Player.PositionInfo(
/* windowUid= */ 1,
/* windowIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(),
/* mediaItemIndex= */ 0,
mediaItems.get(0),
/* periodUid= */ 1,
/* periodIndex= */ 0,
/* positionMs= */ 1234,
@ -787,7 +793,7 @@ public class CastPlayerTest {
Player.PositionInfo newPosition =
new Player.PositionInfo(
/* windowUid= */ null,
/* windowIndex= */ 0,
/* mediaItemIndex= */ 0,
/* mediaItem= */ null,
/* periodUid= */ null,
/* periodIndex= */ 0,
@ -827,10 +833,8 @@ public class CastPlayerTest {
.onMediaItemTransition(
mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
assertThat(mediaItemCaptor.getAllValues().get(0).localConfiguration.tag)
.isEqualTo(mediaItem1.localConfiguration.tag);
assertThat(mediaItemCaptor.getAllValues().get(1).localConfiguration.tag)
.isEqualTo(mediaItem2.localConfiguration.tag);
assertThat(mediaItemCaptor.getAllValues().get(0)).isEqualTo(mediaItem1);
assertThat(mediaItemCaptor.getAllValues().get(1)).isEqualTo(mediaItem2);
}
@Test
@ -862,8 +866,8 @@ public class CastPlayerTest {
Player.PositionInfo oldPosition =
new Player.PositionInfo(
/* windowUid= */ 1,
/* windowIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(),
/* mediaItemIndex= */ 0,
mediaItem1,
/* periodUid= */ 1,
/* periodIndex= */ 0,
/* positionMs= */ 1234,
@ -873,8 +877,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition =
new Player.PositionInfo(
/* windowUid= */ 2,
/* windowIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(),
/* mediaItemIndex= */ 0,
mediaItem2,
/* periodUid= */ 2,
/* periodIndex= */ 0,
/* positionMs= */ 0,
@ -912,10 +916,8 @@ public class CastPlayerTest {
mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
List<MediaItem> capturedMediaItems = mediaItemCaptor.getAllValues();
assertThat(capturedMediaItems.get(0).localConfiguration.tag)
.isEqualTo(mediaItem1.localConfiguration.tag);
assertThat(capturedMediaItems.get(1).localConfiguration.tag)
.isEqualTo(mediaItem2.localConfiguration.tag);
assertThat(capturedMediaItems.get(0)).isEqualTo(mediaItem1);
assertThat(capturedMediaItems.get(1)).isEqualTo(mediaItem2);
}
@Test
@ -945,8 +947,8 @@ public class CastPlayerTest {
Player.PositionInfo oldPosition =
new Player.PositionInfo(
/* windowUid= */ 1,
/* windowIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(),
/* mediaItemIndex= */ 0,
mediaItem1,
/* periodUid= */ 1,
/* periodIndex= */ 0,
/* positionMs= */ 0, // position at which we receive the timeline change
@ -956,8 +958,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition =
new Player.PositionInfo(
/* windowUid= */ 2,
/* windowIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(),
/* mediaItemIndex= */ 0,
mediaItem2,
/* periodUid= */ 2,
/* periodIndex= */ 0,
/* positionMs= */ 0,
@ -992,7 +994,8 @@ public class CastPlayerTest {
InOrder inOrder = Mockito.inOrder(mockListener);
inOrder
.verify(mockListener)
.onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
.onMediaItemTransition(
eq(mediaItem1), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
}
@ -1027,19 +1030,21 @@ public class CastPlayerTest {
castPlayer.addMediaItems(mediaItems);
updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1234);
MediaMetadata firstMediaMetadata = castPlayer.getMediaMetadata();
castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 1234);
MediaMetadata secondMediaMetadata = castPlayer.getMediaMetadata();
InOrder inOrder = Mockito.inOrder(mockListener);
inOrder
.verify(mockListener)
.onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
.onMediaItemTransition(
eq(mediaItem1), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder
.verify(mockListener)
.onMediaItemTransition(
mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK));
.onMediaItemTransition(eq(mediaItem2), eq(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK));
inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
assertThat(mediaItemCaptor.getValue().localConfiguration.tag)
.isEqualTo(mediaItem2.localConfiguration.tag);
assertThat(firstMediaMetadata).isEqualTo(mediaItem1.mediaMetadata);
assertThat(secondMediaMetadata).isEqualTo(mediaItem2.mediaMetadata);
}
@Test
@ -1054,13 +1059,13 @@ public class CastPlayerTest {
castPlayer.addMediaItems(mediaItems);
updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1234);
castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 1234);
Player.PositionInfo oldPosition =
new Player.PositionInfo(
/* windowUid= */ 1,
/* windowIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(),
/* mediaItemIndex= */ 0,
mediaItem1,
/* periodUid= */ 1,
/* periodIndex= */ 0,
/* positionMs= */ 0,
@ -1070,8 +1075,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition =
new Player.PositionInfo(
/* windowUid= */ 2,
/* windowIndex= */ 1,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(),
/* mediaItemIndex= */ 1,
mediaItem2,
/* periodUid= */ 2,
/* periodIndex= */ 1,
/* positionMs= */ 1234,
@ -1097,12 +1102,13 @@ public class CastPlayerTest {
castPlayer.addMediaItems(mediaItems);
updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1234);
castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 1234);
InOrder inOrder = Mockito.inOrder(mockListener);
inOrder
.verify(mockListener)
.onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
.onMediaItemTransition(
eq(mediaItems.get(0)), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
}
@ -1115,14 +1121,13 @@ public class CastPlayerTest {
castPlayer.addMediaItems(mediaItems);
updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1234);
castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 1234);
MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build();
Player.PositionInfo oldPosition =
new Player.PositionInfo(
/* windowUid= */ 1,
/* windowIndex= */ 0,
mediaItem,
/* mediaItemIndex= */ 0,
mediaItems.get(0),
/* periodUid= */ 1,
/* periodIndex= */ 0,
/* positionMs= */ 0,
@ -1132,8 +1137,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition =
new Player.PositionInfo(
/* windowUid= */ 1,
/* windowIndex= */ 0,
mediaItem,
/* mediaItemIndex= */ 0,
mediaItems.get(0),
/* periodUid= */ 1,
/* periodIndex= */ 0,
/* positionMs= */ 1234,
@ -1164,13 +1169,12 @@ public class CastPlayerTest {
InOrder inOrder = Mockito.inOrder(mockListener);
inOrder
.verify(mockListener)
.onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
.onMediaItemTransition(
eq(mediaItems.get(0)), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder
.verify(mockListener)
.onMediaItemTransition(
mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
.onMediaItemTransition(eq(mediaItems.get(1)), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
assertThat(mediaItemCaptor.getValue().localConfiguration.tag).isEqualTo(2);
}
@Test
@ -1203,8 +1207,8 @@ public class CastPlayerTest {
Player.PositionInfo oldPosition =
new Player.PositionInfo(
/* windowUid= */ 1,
/* windowIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(),
/* mediaItemIndex= */ 0,
mediaItems.get(0),
/* periodUid= */ 1,
/* periodIndex= */ 0,
/* positionMs= */ 12500,
@ -1214,8 +1218,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition =
new Player.PositionInfo(
/* windowUid= */ 2,
/* windowIndex= */ 1,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(),
/* mediaItemIndex= */ 1,
mediaItems.get(1),
/* periodUid= */ 2,
/* periodIndex= */ 1,
/* positionMs= */ 0,
@ -1250,12 +1254,11 @@ public class CastPlayerTest {
mediaItems, mediaQueueItemIds, currentItemId, streamTypes, durationsMs, positionMs);
castPlayer.seekBack();
MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build();
Player.PositionInfo oldPosition =
new Player.PositionInfo(
/* windowUid= */ 1,
/* windowIndex= */ 0,
mediaItem,
/* mediaItemIndex= */ 0,
mediaItems.get(0),
/* periodUid= */ 1,
/* periodIndex= */ 0,
/* positionMs= */ 2 * C.DEFAULT_SEEK_BACK_INCREMENT_MS,
@ -1265,8 +1268,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition =
new Player.PositionInfo(
/* windowUid= */ 1,
/* windowIndex= */ 0,
mediaItem,
/* mediaItemIndex= */ 0,
mediaItems.get(0),
/* periodUid= */ 1,
/* periodIndex= */ 0,
/* positionMs= */ C.DEFAULT_SEEK_BACK_INCREMENT_MS,
@ -1299,12 +1302,11 @@ public class CastPlayerTest {
mediaItems, mediaQueueItemIds, currentItemId, streamTypes, durationsMs, positionMs);
castPlayer.seekForward();
MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build();
Player.PositionInfo oldPosition =
new Player.PositionInfo(
/* windowUid= */ 1,
/* windowIndex= */ 0,
mediaItem,
/* mediaItemIndex= */ 0,
mediaItems.get(0),
/* periodUid= */ 1,
/* periodIndex= */ 0,
/* positionMs= */ 0,
@ -1314,8 +1316,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition =
new Player.PositionInfo(
/* windowUid= */ 1,
/* windowIndex= */ 0,
mediaItem,
/* mediaItemIndex= */ 0,
mediaItems.get(0),
/* periodUid= */ 1,
/* periodIndex= */ 0,
/* positionMs= */ C.DEFAULT_SEEK_FORWARD_INCREMENT_MS,
@ -1475,14 +1477,14 @@ public class CastPlayerTest {
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0);
castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousAndNextWindow);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 2, /* positionMs= */ 0);
castPlayer.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 0);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 3, /* positionMs= */ 0);
castPlayer.seekTo(/* mediaItemIndex= */ 3, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousWindow);
verify(mockListener, times(3)).onAvailableCommandsChanged(any());
}
@ -1509,14 +1511,14 @@ public class CastPlayerTest {
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 2, /* positionMs= */ 0);
castPlayer.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousAndNextWindow);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0);
castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0);
castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToNextWindow);
verify(mockListener, times(3)).onAvailableCommandsChanged(any());
}
@ -1533,8 +1535,8 @@ public class CastPlayerTest {
updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
verify(mockListener).onAvailableCommandsChanged(defaultCommands);
castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 200);
castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 100);
castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 200);
castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 100);
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
}
@ -1763,6 +1765,105 @@ public class CastPlayerTest {
verify(mockListener).onAvailableCommandsChanged(any());
}
@Test
public void setMediaItems_doesNotifyOnMetadataChanged() {
when(mockRemoteMediaClient.queueJumpToItem(anyInt(), anyLong(), eq(null)))
.thenReturn(mockPendingResult);
ArgumentCaptor<MediaMetadata> metadataCaptor = ArgumentCaptor.forClass(MediaMetadata.class);
String uri1 = "http://www.google.com/video1";
String uri2 = "http://www.google.com/video2";
ImmutableList<MediaItem> firstPlaylist =
ImmutableList.of(
new MediaItem.Builder()
.setUri(uri1)
.setMimeType(MimeTypes.APPLICATION_MPD)
.setMediaMetadata(new MediaMetadata.Builder().setArtist("foo").build())
.build());
ImmutableList<MediaItem> secondPlaylist =
ImmutableList.of(
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setMediaMetadata(new MediaMetadata.Builder().setArtist("bar").build())
.setMimeType(MimeTypes.APPLICATION_MPD)
.build(),
new MediaItem.Builder()
.setUri(uri2)
.setMimeType(MimeTypes.APPLICATION_MP4)
.setMediaMetadata(new MediaMetadata.Builder().setArtist("foobar").build())
.build());
castPlayer.addListener(mockListener);
MediaMetadata intitalMetadata = castPlayer.getMediaMetadata();
castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 0, /* startPositionMs= */ 2000L);
updateTimeLine(firstPlaylist, /* mediaQueueItemIds= */ new int[] {1}, /* currentItemId= */ 1);
MediaMetadata firstMetadata = castPlayer.getMediaMetadata();
// Replacing existing playlist.
castPlayer.setMediaItems(secondPlaylist, /* startIndex= */ 1, /* startPositionMs= */ 0L);
updateTimeLine(
secondPlaylist, /* mediaQueueItemIds= */ new int[] {2, 3}, /* currentItemId= */ 3);
MediaMetadata secondMetadata = castPlayer.getMediaMetadata();
castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 0);
MediaMetadata thirdMetadata = castPlayer.getMediaMetadata();
verify(mockListener, times(3)).onMediaItemTransition(mediaItemCaptor.capture(), anyInt());
assertThat(mediaItemCaptor.getAllValues())
.containsExactly(firstPlaylist.get(0), secondPlaylist.get(1), secondPlaylist.get(0))
.inOrder();
verify(mockListener, times(3)).onMediaMetadataChanged(metadataCaptor.capture());
assertThat(metadataCaptor.getAllValues())
.containsExactly(
firstPlaylist.get(0).mediaMetadata,
secondPlaylist.get(1).mediaMetadata,
secondPlaylist.get(0).mediaMetadata)
.inOrder();
assertThat(intitalMetadata).isEqualTo(MediaMetadata.EMPTY);
assertThat(ImmutableList.of(firstMetadata, secondMetadata, thirdMetadata))
.containsExactly(
firstPlaylist.get(0).mediaMetadata,
secondPlaylist.get(1).mediaMetadata,
secondPlaylist.get(0).mediaMetadata)
.inOrder();
}
@Test
public void setMediaItems_equalMetadata_doesNotNotifyOnMediaMetadataChanged() {
when(mockRemoteMediaClient.queueJumpToItem(anyInt(), anyLong(), eq(null)))
.thenReturn(mockPendingResult);
String uri1 = "http://www.google.com/video1";
String uri2 = "http://www.google.com/video2";
ImmutableList<MediaItem> firstPlaylist =
ImmutableList.of(
new MediaItem.Builder()
.setUri(uri1)
.setMimeType(MimeTypes.APPLICATION_MPD)
.setTag(1)
.build());
ImmutableList<MediaItem> secondPlaylist =
ImmutableList.of(
new MediaItem.Builder()
.setMediaMetadata(MediaMetadata.EMPTY)
.setUri(Uri.EMPTY)
.setTag(2)
.setMimeType(MimeTypes.APPLICATION_MPD)
.build(),
new MediaItem.Builder()
.setUri(uri2)
.setMimeType(MimeTypes.APPLICATION_MP4)
.setTag(3)
.build());
castPlayer.addListener(mockListener);
castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 0, /* startPositionMs= */ 2000L);
updateTimeLine(firstPlaylist, /* mediaQueueItemIds= */ new int[] {1}, /* currentItemId= */ 1);
castPlayer.setMediaItems(secondPlaylist, /* startIndex= */ 1, /* startPositionMs= */ 0L);
updateTimeLine(
secondPlaylist, /* mediaQueueItemIds= */ new int[] {2, 3}, /* currentItemId= */ 3);
castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 0);
verify(mockListener, times(3)).onMediaItemTransition(any(), anyInt());
verify(mockListener, never()).onMediaMetadataChanged(any());
}
private int[] createMediaQueueItemIds(int numberOfIds) {
int[] mediaQueueItemIds = new int[numberOfIds];
for (int i = 0; i < numberOfIds; i++) {
@ -1782,8 +1883,9 @@ public class CastPlayerTest {
private MediaItem createMediaItem(int mediaQueueItemId) {
return new MediaItem.Builder()
.setUri("http://www.google.com/video" + mediaQueueItemId)
.setMediaMetadata(
new MediaMetadata.Builder().setArtist("Foo Bar - " + mediaQueueItemId).build())
.setMimeType(MimeTypes.APPLICATION_MPD)
.setTag(mediaQueueItemId)
.build();
}
@ -1821,8 +1923,12 @@ public class CastPlayerTest {
int mediaQueueItemId = mediaQueueItemIds[i];
int streamType = streamTypes[i];
long durationMs = durationsMs[i];
String contentId =
mediaItem.mediaId.equals(MediaItem.DEFAULT_MEDIA_ID)
? mediaItem.localConfiguration.uri.toString()
: mediaItem.mediaId;
MediaInfo.Builder mediaInfoBuilder =
new MediaInfo.Builder(mediaItem.localConfiguration.uri.toString())
new MediaInfo.Builder(contentId)
.setStreamType(streamType)
.setContentType(mediaItem.localConfiguration.mimeType);
if (durationMs != C.TIME_UNSET) {

View File

@ -15,21 +15,30 @@
*/
package androidx.media3.cast;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.Timeline.Window;
import androidx.media3.common.util.Util;
import androidx.media3.test.utils.TimelineAsserts;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.framework.media.MediaQueue;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
/** Tests for {@link CastTimelineTracker}. */
@RunWith(AndroidJUnit4.class)
@ -40,10 +49,19 @@ public class CastTimelineTrackerTest {
private static final long DURATION_4_MS = 4000;
private static final long DURATION_5_MS = 5000;
private MediaItemConverter mediaItemConverter;
private CastTimelineTracker castTimelineTracker;
@Before
public void init() {
mediaItemConverter = new DefaultMediaItemConverter();
castTimelineTracker = new CastTimelineTracker(mediaItemConverter);
}
/** Tests that duration of the current media info is correctly propagated to the timeline. */
@Test
public void getCastTimelinePersistsDuration() {
CastTimelineTracker tracker = new CastTimelineTracker();
CastTimelineTracker tracker = new CastTimelineTracker(new DefaultMediaItemConverter());
RemoteMediaClient remoteMediaClient =
mockRemoteMediaClient(
@ -104,10 +122,179 @@ public class CastTimelineTrackerTest {
Util.msToUs(DURATION_5_MS));
}
@Test
public void getCastTimeline_onMediaItemsSet_correctMediaItemsInTimeline() {
RemoteMediaClient mockRemoteMediaClient = mock(RemoteMediaClient.class);
MediaQueue mockMediaQueue = mock(MediaQueue.class);
MediaStatus mockMediaStatus = mock(MediaStatus.class);
ImmutableList<MediaItem> playlistMediaItems =
ImmutableList.of(createMediaItem(0), createMediaItem(1));
MediaQueueItem[] playlistMediaQueueItems =
new MediaQueueItem[] {
createMediaQueueItem(playlistMediaItems.get(0), 0),
createMediaQueueItem(playlistMediaItems.get(1), 1)
};
castTimelineTracker.onMediaItemsSet(playlistMediaItems, playlistMediaQueueItems);
// Mock remote media client state after adding two items.
when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue);
when(mockMediaQueue.getItemIds()).thenReturn(new int[] {0, 1});
when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus);
when(mockMediaStatus.getCurrentItemId()).thenReturn(0);
when(mockMediaStatus.getMediaInfo()).thenReturn(playlistMediaQueueItems[0].getMedia());
when(mockMediaStatus.getQueueItems()).thenReturn(Arrays.asList(playlistMediaQueueItems));
CastTimeline castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient);
assertThat(castTimeline.getWindowCount()).isEqualTo(2);
assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem)
.isEqualTo(playlistMediaItems.get(0));
assertThat(castTimeline.getWindow(/* windowIndex= */ 1, new Window()).mediaItem)
.isEqualTo(playlistMediaItems.get(1));
MediaItem thirdMediaItem = createMediaItem(2);
MediaQueueItem thirdMediaQueueItem = createMediaQueueItem(thirdMediaItem, 2);
castTimelineTracker.onMediaItemsSet(
ImmutableList.of(thirdMediaItem), new MediaQueueItem[] {thirdMediaQueueItem});
// Mock remote media client state after a single item overrides the previous playlist.
when(mockMediaQueue.getItemIds()).thenReturn(new int[] {2});
when(mockMediaStatus.getCurrentItemId()).thenReturn(2);
when(mockMediaStatus.getMediaInfo()).thenReturn(thirdMediaQueueItem.getMedia());
when(mockMediaStatus.getQueueItems()).thenReturn(ImmutableList.of(thirdMediaQueueItem));
castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient);
assertThat(castTimeline.getWindowCount()).isEqualTo(1);
assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem)
.isEqualTo(thirdMediaItem);
}
@Test
public void getCastTimeline_onMediaItemsAdded_correctMediaItemsInTimeline() {
RemoteMediaClient mockRemoteMediaClient = mock(RemoteMediaClient.class);
MediaQueue mockMediaQueue = mock(MediaQueue.class);
MediaStatus mockMediaStatus = mock(MediaStatus.class);
ImmutableList<MediaItem> playlistMediaItems =
ImmutableList.of(createMediaItem(0), createMediaItem(1));
MediaQueueItem[] playlistQueueItems =
new MediaQueueItem[] {
createMediaQueueItem(playlistMediaItems.get(0), /* uid= */ 0),
createMediaQueueItem(playlistMediaItems.get(1), /* uid= */ 1)
};
ImmutableList<MediaItem> secondPlaylistMediaItems =
new ImmutableList.Builder<MediaItem>()
.addAll(playlistMediaItems)
.add(createMediaItem(2))
.build();
castTimelineTracker.onMediaItemsAdded(playlistMediaItems, playlistQueueItems);
when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue);
when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus);
// Mock remote media client state after two items have been added.
when(mockMediaQueue.getItemIds()).thenReturn(new int[] {0, 1});
when(mockMediaStatus.getCurrentItemId()).thenReturn(0);
when(mockMediaStatus.getMediaInfo()).thenReturn(playlistQueueItems[0].getMedia());
when(mockMediaStatus.getQueueItems()).thenReturn(Arrays.asList(playlistQueueItems));
CastTimeline castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient);
assertThat(castTimeline.getWindowCount()).isEqualTo(2);
assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem)
.isEqualTo(playlistMediaItems.get(0));
assertThat(castTimeline.getWindow(/* windowIndex= */ 1, new Window()).mediaItem)
.isEqualTo(playlistMediaItems.get(1));
// Mock remote media client state after adding a third item.
List<MediaQueueItem> playlistThreeQueueItems =
new ArrayList<>(Arrays.asList(playlistQueueItems));
playlistThreeQueueItems.add(createMediaQueueItem(secondPlaylistMediaItems.get(2), 2));
castTimelineTracker.onMediaItemsAdded(
secondPlaylistMediaItems, playlistThreeQueueItems.toArray(new MediaQueueItem[0]));
when(mockMediaQueue.getItemIds()).thenReturn(new int[] {0, 1, 2});
when(mockMediaStatus.getQueueItems()).thenReturn(playlistThreeQueueItems);
castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient);
assertThat(castTimeline.getWindowCount()).isEqualTo(3);
assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem)
.isEqualTo(secondPlaylistMediaItems.get(0));
assertThat(castTimeline.getWindow(/* windowIndex= */ 1, new Window()).mediaItem)
.isEqualTo(secondPlaylistMediaItems.get(1));
assertThat(castTimeline.getWindow(/* windowIndex= */ 2, new Window()).mediaItem)
.isEqualTo(secondPlaylistMediaItems.get(2));
}
@Test
public void getCastTimeline_itemsRemoved_correctMediaItemsInTimelineAndMapCleanedUp() {
RemoteMediaClient mockRemoteMediaClient = mock(RemoteMediaClient.class);
MediaQueue mockMediaQueue = mock(MediaQueue.class);
MediaStatus mockMediaStatus = mock(MediaStatus.class);
ImmutableList<MediaItem> playlistMediaItems =
ImmutableList.of(createMediaItem(0), createMediaItem(1));
MediaQueueItem[] initialPlaylistTwoQueueItems =
new MediaQueueItem[] {
createMediaQueueItem(playlistMediaItems.get(0), 0),
createMediaQueueItem(playlistMediaItems.get(1), 1)
};
castTimelineTracker.onMediaItemsSet(playlistMediaItems, initialPlaylistTwoQueueItems);
when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue);
when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus);
// Mock remote media client state with two items in the queue.
when(mockMediaQueue.getItemIds()).thenReturn(new int[] {0, 1});
when(mockMediaStatus.getCurrentItemId()).thenReturn(0);
when(mockMediaStatus.getMediaInfo()).thenReturn(initialPlaylistTwoQueueItems[0].getMedia());
when(mockMediaStatus.getQueueItems()).thenReturn(Arrays.asList(initialPlaylistTwoQueueItems));
CastTimeline castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient);
assertThat(castTimeline.getWindowCount()).isEqualTo(2);
assertThat(castTimelineTracker.mediaItemsByContentId).hasSize(2);
// Mock remote media client state after the first item has been removed.
when(mockMediaQueue.getItemIds()).thenReturn(new int[] {1});
when(mockMediaStatus.getCurrentItemId()).thenReturn(1);
when(mockMediaStatus.getMediaInfo()).thenReturn(initialPlaylistTwoQueueItems[1].getMedia());
when(mockMediaStatus.getQueueItems())
.thenReturn(ImmutableList.of(initialPlaylistTwoQueueItems[1]));
castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient);
assertThat(castTimeline.getWindowCount()).isEqualTo(1);
assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem)
.isEqualTo(playlistMediaItems.get(1));
// Assert that the removed item has been removed from the content ID map.
assertThat(castTimelineTracker.mediaItemsByContentId).hasSize(1);
// Mock remote media client state for empty queue.
when(mockRemoteMediaClient.getMediaStatus()).thenReturn(null);
when(mockMediaQueue.getItemIds()).thenReturn(new int[0]);
when(mockMediaStatus.getCurrentItemId()).thenReturn(MediaQueueItem.INVALID_ITEM_ID);
when(mockMediaStatus.getMediaInfo()).thenReturn(null);
when(mockMediaStatus.getQueueItems()).thenReturn(ImmutableList.of());
castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient);
assertThat(castTimeline.getWindowCount()).isEqualTo(0);
// Queue is not emptied when remote media client is empty. See [Internal ref: b/128825216].
assertThat(castTimelineTracker.mediaItemsByContentId).hasSize(1);
}
private MediaItem createMediaItem(int uid) {
return new MediaItem.Builder()
.setUri("http://www.google.com/" + uid)
.setMimeType(MimeTypes.AUDIO_MPEG)
.setTag(uid)
.build();
}
private MediaQueueItem createMediaQueueItem(MediaItem mediaItem, int uid) {
return new MediaQueueItem.Builder(mediaItemConverter.toMediaQueueItem(mediaItem))
.setItemId(uid)
.build();
}
private static RemoteMediaClient mockRemoteMediaClient(
int[] itemIds, int currentItemId, long currentDurationMs) {
RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);
MediaStatus status = Mockito.mock(MediaStatus.class);
RemoteMediaClient remoteMediaClient = mock(RemoteMediaClient.class);
MediaStatus status = mock(MediaStatus.class);
when(status.getQueueItems()).thenReturn(Collections.emptyList());
when(remoteMediaClient.getMediaStatus()).thenReturn(status);
when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
@ -118,7 +305,7 @@ public class CastTimelineTrackerTest {
}
private static MediaQueue mockMediaQueue(int[] itemIds) {
MediaQueue mediaQueue = Mockito.mock(MediaQueue.class);
MediaQueue mediaQueue = mock(MediaQueue.class);
when(mediaQueue.getItemIds()).thenReturn(itemIds);
return mediaQueue;
}

View File

@ -50,6 +50,7 @@ public class DefaultMediaItemConverterTest {
MediaItem.Builder builder = new MediaItem.Builder();
MediaItem item =
builder
.setMediaId("fooBar")
.setUri(Uri.parse("http://example.com"))
.setMediaMetadata(MediaMetadata.EMPTY)
.setMimeType(MimeTypes.APPLICATION_MPD)
@ -66,4 +67,45 @@ public class DefaultMediaItemConverterTest {
assertThat(reconstructedItem).isEqualTo(item);
}
@Test
public void toMediaQueueItem_nonDefaultMediaId_usedAsContentId() {
MediaItem.Builder builder = new MediaItem.Builder();
MediaItem item =
builder
.setMediaId("fooBar")
.setUri("http://example.com")
.setMimeType(MimeTypes.APPLICATION_MPD)
.build();
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
MediaQueueItem queueItem = converter.toMediaQueueItem(item);
assertThat(queueItem.getMedia().getContentId()).isEqualTo("fooBar");
}
@Test
public void toMediaQueueItem_defaultMediaId_uriAsContentId() {
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
MediaItem mediaItem =
new MediaItem.Builder()
.setUri("http://example.com")
.setMimeType(MimeTypes.APPLICATION_MPD)
.build();
MediaQueueItem queueItem = converter.toMediaQueueItem(mediaItem);
assertThat(queueItem.getMedia().getContentId()).isEqualTo("http://example.com");
MediaItem secondMediaItem =
new MediaItem.Builder()
.setMediaId(MediaItem.DEFAULT_MEDIA_ID)
.setUri("http://example.com")
.setMimeType(MimeTypes.APPLICATION_MPD)
.build();
MediaQueueItem secondQueueItem = converter.toMediaQueueItem(secondMediaItem);
assertThat(secondQueueItem.getMedia().getContentId()).isEqualTo("http://example.com");
}
}

View File

@ -75,6 +75,7 @@ dependencies {
testImplementation 'junit:junit:' + junitVersion
testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'lib-exoplayer')
testImplementation project(modulePrefix + 'test-utils')
}

View File

@ -18,6 +18,7 @@ package androidx.media3.common;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import androidx.media3.common.DrmInitData.SchemeData;
import androidx.media3.common.util.Assertions;
@ -157,6 +158,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
* @param schemeType A protection scheme type. May be null.
* @return A copy with the specified protection scheme type.
*/
@CheckResult
public DrmInitData copyWithSchemeType(@Nullable String schemeType) {
if (Util.areEqual(this.schemeType, schemeType)) {
return this;
@ -333,6 +335,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
* @param data The data to include in the copy.
* @return The new instance.
*/
@CheckResult
public SchemeData copyWithData(@Nullable byte[] data) {
return new SchemeData(uuid, licenseServerUrl, mimeType, data);
}

View File

@ -29,11 +29,11 @@ public final class MediaLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3" or "1.2.3-beta01". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "1.0.0-beta01";
public static final String VERSION = "1.0.0-beta02";
/** The version of the library expressed as {@code TAG + "/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-beta01";
public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-beta02";
/**
* The version of the library expressed as an integer, for example 1002003300.
@ -47,7 +47,7 @@ public final class MediaLibraryInfo {
* (123-045-006-3-00).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 1_000_000_1_01;
public static final int VERSION_INT = 1_000_000_1_02;
/** Whether the library was compiled with {@link Assertions} checks enabled. */
public static final boolean ASSERTIONS_ENABLED = true;

View File

@ -373,6 +373,7 @@ public interface Player {
COMMAND_GET_TIMELINE,
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEM,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_AUDIO_ATTRIBUTES,
COMMAND_GET_VOLUME,
@ -384,7 +385,6 @@ public interface Player {
COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACKS,
COMMAND_SET_MEDIA_ITEM,
};
private final FlagSet.Builder flagsBuilder;
@ -1432,6 +1432,7 @@ public interface Player {
COMMAND_GET_TIMELINE,
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEM,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_AUDIO_ATTRIBUTES,
COMMAND_GET_VOLUME,
@ -1443,7 +1444,6 @@ public interface Player {
COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACKS,
COMMAND_SET_MEDIA_ITEM,
})
@interface Command {}
/** Command to start, pause or resume playback. */
@ -1501,6 +1501,8 @@ public interface Player {
int COMMAND_GET_MEDIA_ITEMS_METADATA = 18;
/** Command to set the {@link MediaItem MediaItems} metadata. */
int COMMAND_SET_MEDIA_ITEMS_METADATA = 19;
/** Command to set a {@link MediaItem MediaItem}. */
int COMMAND_SET_MEDIA_ITEM = 31;
/** Command to change the {@link MediaItem MediaItems} in the playlist. */
int COMMAND_CHANGE_MEDIA_ITEMS = 20;
/** Command to get the player current {@link AudioAttributes}. */
@ -1523,8 +1525,6 @@ public interface Player {
int COMMAND_SET_TRACK_SELECTION_PARAMETERS = 29;
/** Command to get details of the current track selection. */
int COMMAND_GET_TRACKS = 30;
/** Command to set a {@link MediaItem MediaItem}. */
int COMMAND_SET_MEDIA_ITEM = 31;
/** Represents an invalid {@link Command}. */
int COMMAND_INVALID = -1;

View File

@ -1351,6 +1351,27 @@ public abstract class Timeline implements Bundleable {
return false;
}
}
// Check shuffled order
int windowIndex = getFirstWindowIndex(/* shuffleModeEnabled= */ true);
if (windowIndex != other.getFirstWindowIndex(/* shuffleModeEnabled= */ true)) {
return false;
}
int lastWindowIndex = getLastWindowIndex(/* shuffleModeEnabled= */ true);
if (lastWindowIndex != other.getLastWindowIndex(/* shuffleModeEnabled= */ true)) {
return false;
}
while (windowIndex != lastWindowIndex) {
int nextWindowIndex =
getNextWindowIndex(windowIndex, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true);
if (nextWindowIndex
!= other.getNextWindowIndex(
windowIndex, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) {
return false;
}
windowIndex = nextWindowIndex;
}
return true;
}
@ -1367,6 +1388,13 @@ public abstract class Timeline implements Bundleable {
for (int i = 0; i < getPeriodCount(); i++) {
result = 31 * result + getPeriod(i, period, /* setIds= */ true).hashCode();
}
for (int windowIndex = getFirstWindowIndex(true);
windowIndex != C.INDEX_UNSET;
windowIndex = getNextWindowIndex(windowIndex, Player.REPEAT_MODE_OFF, true)) {
result = 31 * result + windowIndex;
}
return result;
}

View File

@ -18,6 +18,7 @@ package androidx.media3.common.util;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.text.TextUtils;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.Size;
@ -28,7 +29,10 @@ import java.lang.annotation.Target;
import java.net.UnknownHostException;
import org.checkerframework.dataflow.qual.Pure;
/** Wrapper around {@link android.util.Log} which allows to set the log level. */
/**
* Wrapper around {@link android.util.Log} which allows to set the log level and to specify a custom
* log output.
*/
@UnstableApi
public final class Log {
@ -52,15 +56,89 @@ public final class Log {
/** Log level to disable all logging. */
public static final int LOG_LEVEL_OFF = Integer.MAX_VALUE;
/**
* Interface for a logger that can output messages with a tag.
*
* <p>Use {@link #DEFAULT} to output to {@link android.util.Log}.
*/
public interface Logger {
/** The default instance logging to {@link android.util.Log}. */
Logger DEFAULT =
new Logger() {
@Override
public void d(String tag, String message) {
android.util.Log.d(tag, message);
}
@Override
public void i(String tag, String message) {
android.util.Log.i(tag, message);
}
@Override
public void w(String tag, String message) {
android.util.Log.w(tag, message);
}
@Override
public void e(String tag, String message) {
android.util.Log.e(tag, message);
}
};
/**
* Logs a debug-level message.
*
* @param tag The tag of the message.
* @param message The message.
*/
void d(String tag, String message);
/**
* Logs an information-level message.
*
* @param tag The tag of the message.
* @param message The message.
*/
void i(String tag, String message);
/**
* Logs a warning-level message.
*
* @param tag The tag of the message.
* @param message The message.
*/
void w(String tag, String message);
/**
* Logs an error-level message.
*
* @param tag The tag of the message.
* @param message The message.
*/
void e(String tag, String message);
}
private static final Object lock = new Object();
@GuardedBy("lock")
private static int logLevel = LOG_LEVEL_ALL;
@GuardedBy("lock")
private static boolean logStackTraces = true;
@GuardedBy("lock")
private static Logger logger = Logger.DEFAULT;
private Log() {}
/** Returns current {@link LogLevel} for ExoPlayer logcat logging. */
@Pure
public static @LogLevel int getLogLevel() {
return logLevel;
synchronized (lock) {
return logLevel;
}
}
/**
@ -69,7 +147,9 @@ public final class Log {
* @param logLevel The new {@link LogLevel}.
*/
public static void setLogLevel(@LogLevel int logLevel) {
Log.logLevel = logLevel;
synchronized (lock) {
Log.logLevel = logLevel;
}
}
/**
@ -79,7 +159,20 @@ public final class Log {
* @param logStackTraces Whether stack traces will be logged.
*/
public static void setLogStackTraces(boolean logStackTraces) {
Log.logStackTraces = logStackTraces;
synchronized (lock) {
Log.logStackTraces = logStackTraces;
}
}
/**
* Sets a custom {@link Logger} as the output.
*
* @param logger The {@link Logger}.
*/
public static void setLogger(Logger logger) {
synchronized (lock) {
Log.logger = logger;
}
}
/**
@ -87,8 +180,10 @@ public final class Log {
*/
@Pure
public static void d(@Size(max = 23) String tag, String message) {
if (logLevel == LOG_LEVEL_ALL) {
android.util.Log.d(tag, message);
synchronized (lock) {
if (logLevel == LOG_LEVEL_ALL) {
logger.d(tag, message);
}
}
}
@ -105,8 +200,10 @@ public final class Log {
*/
@Pure
public static void i(@Size(max = 23) String tag, String message) {
if (logLevel <= LOG_LEVEL_INFO) {
android.util.Log.i(tag, message);
synchronized (lock) {
if (logLevel <= LOG_LEVEL_INFO) {
logger.i(tag, message);
}
}
}
@ -123,8 +220,10 @@ public final class Log {
*/
@Pure
public static void w(@Size(max = 23) String tag, String message) {
if (logLevel <= LOG_LEVEL_WARNING) {
android.util.Log.w(tag, message);
synchronized (lock) {
if (logLevel <= LOG_LEVEL_WARNING) {
logger.w(tag, message);
}
}
}
@ -141,8 +240,10 @@ public final class Log {
*/
@Pure
public static void e(@Size(max = 23) String tag, String message) {
if (logLevel <= LOG_LEVEL_ERROR) {
android.util.Log.e(tag, message);
synchronized (lock) {
if (logLevel <= LOG_LEVEL_ERROR) {
logger.e(tag, message);
}
}
}
@ -168,20 +269,23 @@ public final class Log {
@Nullable
@Pure
public static String getThrowableString(@Nullable Throwable throwable) {
if (throwable == null) {
return null;
} else if (isCausedByUnknownHostException(throwable)) {
// UnknownHostException implies the device doesn't have network connectivity.
// UnknownHostException.getMessage() may return a string that's more verbose than desired for
// logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has
// special handling to return the empty string, which can result in logging that doesn't
// indicate the failure mode at all. Hence we special case this exception to always return a
// concise but useful message.
return "UnknownHostException (no network)";
} else if (!logStackTraces) {
return throwable.getMessage();
} else {
return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " ");
synchronized (lock) {
if (throwable == null) {
return null;
} else if (isCausedByUnknownHostException(throwable)) {
// UnknownHostException implies the device doesn't have network connectivity.
// UnknownHostException.getMessage() may return a string that's more verbose than desired
// for
// logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has
// special handling to return the empty string, which can result in logging that doesn't
// indicate the failure mode at all. Hence we special case this exception to always return a
// concise but useful message.
return "UnknownHostException (no network)";
} else if (!logStackTraces) {
return throwable.getMessage();
} else {
return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " ");
}
}
}

View File

@ -55,6 +55,7 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Parcel;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.security.NetworkSecurityPolicy;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@ -199,7 +200,7 @@ public final class Util {
@UnstableApi
@Nullable
public static ComponentName startForegroundService(Context context, Intent intent) {
if (Util.SDK_INT >= 26) {
if (SDK_INT >= 26) {
return context.startForegroundService(intent);
} else {
return context.startService(intent);
@ -215,12 +216,12 @@ public final class Util {
* @return Whether a permission request was made.
*/
public static boolean maybeRequestReadExternalStoragePermission(Activity activity, Uri... uris) {
if (Util.SDK_INT < 23) {
if (SDK_INT < 23) {
return false;
}
for (Uri uri : uris) {
if (isLocalFileUri(uri)) {
return requestExternalStoragePermission(activity);
if (maybeRequestReadExternalStoragePermission(activity, uri)) {
return true;
}
}
return false;
@ -238,25 +239,46 @@ public final class Util {
*/
public static boolean maybeRequestReadExternalStoragePermission(
Activity activity, MediaItem... mediaItems) {
if (Util.SDK_INT < 23) {
if (SDK_INT < 23) {
return false;
}
for (MediaItem mediaItem : mediaItems) {
if (mediaItem.localConfiguration == null) {
continue;
}
if (isLocalFileUri(mediaItem.localConfiguration.uri)) {
return requestExternalStoragePermission(activity);
if (maybeRequestReadExternalStoragePermission(activity, mediaItem.localConfiguration.uri)) {
return true;
}
for (int i = 0; i < mediaItem.localConfiguration.subtitleConfigurations.size(); i++) {
if (isLocalFileUri(mediaItem.localConfiguration.subtitleConfigurations.get(i).uri)) {
return requestExternalStoragePermission(activity);
List<MediaItem.SubtitleConfiguration> subtitleConfigs =
mediaItem.localConfiguration.subtitleConfigurations;
for (int i = 0; i < subtitleConfigs.size(); i++) {
if (maybeRequestReadExternalStoragePermission(activity, subtitleConfigs.get(i).uri)) {
return true;
}
}
}
return false;
}
private static boolean maybeRequestReadExternalStoragePermission(Activity activity, Uri uri) {
return SDK_INT >= 23
&& (isLocalFileUri(uri) || isMediaStoreExternalContentUri(uri))
&& requestExternalStoragePermission(activity);
}
private static boolean isMediaStoreExternalContentUri(Uri uri) {
if (!"content".equals(uri.getScheme()) || !MediaStore.AUTHORITY.equals(uri.getAuthority())) {
return false;
}
List<String> pathSegments = uri.getPathSegments();
if (pathSegments.isEmpty()) {
return false;
}
String firstPathSegment = pathSegments.get(0);
return MediaStore.VOLUME_EXTERNAL.equals(firstPathSegment)
|| MediaStore.VOLUME_EXTERNAL_PRIMARY.equals(firstPathSegment);
}
/**
* Returns whether it may be possible to load the URIs of the given media items based on the
* network security policy's cleartext traffic permissions.
@ -265,7 +287,7 @@ public final class Util {
* @return Whether it may be possible to load the URIs of the given media items.
*/
public static boolean checkCleartextTrafficPermitted(MediaItem... mediaItems) {
if (Util.SDK_INT < 24) {
if (SDK_INT < 24) {
// We assume cleartext traffic is permitted.
return true;
}
@ -650,7 +672,7 @@ public final class Util {
normalizedTag = language;
}
normalizedTag = Ascii.toLowerCase(normalizedTag);
String mainLanguage = Util.splitAtFirst(normalizedTag, "-")[0];
String mainLanguage = splitAtFirst(normalizedTag, "-")[0];
if (languageTagReplacementMap == null) {
languageTagReplacementMap = createIsoLanguageReplacementMap();
}
@ -1712,9 +1734,9 @@ public final class Util {
case 7:
return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
case 8:
if (Util.SDK_INT >= 23) {
if (SDK_INT >= 23) {
return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
} else if (Util.SDK_INT >= 21) {
} else if (SDK_INT >= 21) {
// Equal to AudioFormat.CHANNEL_OUT_7POINT1_SURROUND, which is hidden before Android M.
return AudioFormat.CHANNEL_OUT_5POINT1
| AudioFormat.CHANNEL_OUT_SIDE_LEFT
@ -2005,7 +2027,7 @@ public final class Util {
public static @ContentType int inferContentTypeForUriAndMimeType(
Uri uri, @Nullable String mimeType) {
if (mimeType == null) {
return Util.inferContentType(uri);
return inferContentType(uri);
}
switch (mimeType) {
case MimeTypes.APPLICATION_MPD:
@ -2345,7 +2367,7 @@ public final class Util {
/** Returns the default {@link Locale.Category#DISPLAY DISPLAY} {@link Locale}. */
@UnstableApi
public static Locale getDefaultDisplayLocale() {
return Util.SDK_INT >= 24 ? Locale.getDefault(Locale.Category.DISPLAY) : Locale.getDefault();
return SDK_INT >= 24 ? Locale.getDefault(Locale.Category.DISPLAY) : Locale.getDefault();
}
/**
@ -2420,7 +2442,7 @@ public final class Util {
*/
@UnstableApi
public static boolean isAutomotive(Context context) {
return Util.SDK_INT >= 23
return SDK_INT >= 23
&& context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
}
@ -2439,7 +2461,7 @@ public final class Util {
@UnstableApi
public static Point getCurrentDisplayModeSize(Context context) {
@Nullable Display defaultDisplay = null;
if (Util.SDK_INT >= 17) {
if (SDK_INT >= 17) {
@Nullable
DisplayManager displayManager =
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
@ -2488,7 +2510,7 @@ public final class Util {
// vendor.display-size instead.
@Nullable
String displaySize =
Util.SDK_INT < 28
SDK_INT < 28
? getSystemProperty("sys.display-size")
: getSystemProperty("vendor.display-size");
// If we managed to read the display size, attempt to parse it.
@ -2509,17 +2531,17 @@ public final class Util {
}
// Sony Android TVs advertise support for 4k output via a system feature.
if ("Sony".equals(Util.MANUFACTURER)
&& Util.MODEL.startsWith("BRAVIA")
if ("Sony".equals(MANUFACTURER)
&& MODEL.startsWith("BRAVIA")
&& context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd")) {
return new Point(3840, 2160);
}
}
Point displaySize = new Point();
if (Util.SDK_INT >= 23) {
if (SDK_INT >= 23) {
getDisplaySizeV23(display, displaySize);
} else if (Util.SDK_INT >= 17) {
} else if (SDK_INT >= 17) {
getDisplaySizeV17(display, displaySize);
} else {
getDisplaySizeV16(display, displaySize);
@ -2745,7 +2767,7 @@ public final class Util {
@RequiresApi(24)
private static String[] getSystemLocalesV24(Configuration config) {
return Util.split(config.getLocales().toLanguageTags(), ",");
return split(config.getLocales().toLanguageTags(), ",");
}
@RequiresApi(21)

View File

@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem.LiveConfiguration;
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder;
import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
import androidx.media3.test.utils.TimelineAsserts;
@ -64,6 +65,50 @@ public class TimelineTest {
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
}
@Test
public void timelineEquals() {
ImmutableList<TimelineWindowDefinition> timelineWindowDefinitions =
ImmutableList.of(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 111),
new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 222),
new TimelineWindowDefinition(/* periodCount= */ 3, /* id= */ 333));
Timeline timeline1 =
new FakeTimeline(timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
Timeline timeline2 =
new FakeTimeline(timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
assertThat(timeline1).isEqualTo(timeline2);
assertThat(timeline1.hashCode()).isEqualTo(timeline2.hashCode());
}
@Test
public void timelineEquals_includesShuffleOrder() {
ImmutableList<TimelineWindowDefinition> timelineWindowDefinitions =
ImmutableList.of(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 111),
new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 222),
new TimelineWindowDefinition(/* periodCount= */ 3, /* id= */ 333));
Timeline timeline =
new FakeTimeline(
new Object[0],
new DefaultShuffleOrder(timelineWindowDefinitions.size(), /* randomSeed= */ 5),
timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
Timeline timelineWithEquivalentShuffleOrder =
new FakeTimeline(
new Object[0],
new DefaultShuffleOrder(timelineWindowDefinitions.size(), /* randomSeed= */ 5),
timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
Timeline timelineWithDifferentShuffleOrder =
new FakeTimeline(
new Object[0],
new DefaultShuffleOrder(timelineWindowDefinitions.size(), /* randomSeed= */ 3),
timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
assertThat(timeline).isEqualTo(timelineWithEquivalentShuffleOrder);
assertThat(timeline.hashCode()).isEqualTo(timelineWithEquivalentShuffleOrder.hashCode());
assertThat(timeline).isNotEqualTo(timelineWithDifferentShuffleOrder);
}
@Test
public void windowEquals() {
MediaItem mediaItem = new MediaItem.Builder().setUri("uri").setTag(new Object()).build();

View File

@ -294,6 +294,7 @@ import java.util.concurrent.TimeoutException;
COMMAND_GET_TIMELINE,
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEM,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_TRACKS,
COMMAND_GET_AUDIO_ATTRIBUTES,
@ -303,8 +304,7 @@ import java.util.concurrent.TimeoutException;
COMMAND_SET_DEVICE_VOLUME,
COMMAND_ADJUST_DEVICE_VOLUME,
COMMAND_SET_VIDEO_SURFACE,
COMMAND_GET_TEXT,
COMMAND_SET_MEDIA_ITEM)
COMMAND_GET_TEXT)
.addIf(
COMMAND_SET_TRACK_SELECTION_PARAMETERS, trackSelector.isSetParametersSupported())
.build();
@ -433,6 +433,9 @@ import java.util.concurrent.TimeoutException;
public void experimentalSetOffloadSchedulingEnabled(boolean offloadSchedulingEnabled) {
verifyApplicationThread();
internalPlayer.experimentalSetOffloadSchedulingEnabled(offloadSchedulingEnabled);
for (AudioOffloadListener listener : audioOffloadListeners) {
listener.onExperimentalOffloadSchedulingEnabledChanged(offloadSchedulingEnabled);
}
}
@Override
@ -707,6 +710,7 @@ import java.util.concurrent.TimeoutException;
@Override
public void setShuffleOrder(ShuffleOrder shuffleOrder) {
verifyApplicationThread();
this.shuffleOrder = shuffleOrder;
Timeline timeline = createMaskingTimeline();
PlaybackInfo newPlaybackInfo =
maskTimelineAndPosition(
@ -715,7 +719,6 @@ import java.util.concurrent.TimeoutException;
maskWindowPositionMsOrGetPeriodPositionUs(
timeline, getCurrentMediaItemIndex(), getCurrentPosition()));
pendingOperationAcks++;
this.shuffleOrder = shuffleOrder;
internalPlayer.setShuffleOrder(shuffleOrder);
updatePlaybackInfo(
newPlaybackInfo,
@ -1962,12 +1965,6 @@ import java.util.concurrent.TimeoutException;
updateAvailableCommands();
listeners.flushEvents();
if (previousPlaybackInfo.offloadSchedulingEnabled != newPlaybackInfo.offloadSchedulingEnabled) {
for (AudioOffloadListener listener : audioOffloadListeners) {
listener.onExperimentalOffloadSchedulingEnabledChanged(
newPlaybackInfo.offloadSchedulingEnabled);
}
}
if (previousPlaybackInfo.sleepingForOffload != newPlaybackInfo.sleepingForOffload) {
for (AudioOffloadListener listener : audioOffloadListeners) {
listener.onExperimentalSleepingForOffloadChanged(newPlaybackInfo.sleepingForOffload);

View File

@ -817,10 +817,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
return;
}
this.offloadSchedulingEnabled = offloadSchedulingEnabled;
@Player.State int state = playbackInfo.playbackState;
if (offloadSchedulingEnabled || state == Player.STATE_ENDED || state == Player.STATE_IDLE) {
playbackInfo = playbackInfo.copyWithOffloadSchedulingEnabled(offloadSchedulingEnabled);
} else {
if (!offloadSchedulingEnabled && playbackInfo.sleepingForOffload) {
// We need to wake the player up if offload scheduling is disabled and we are sleeping.
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
}
@ -960,12 +958,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void doSomeWork() throws ExoPlaybackException, IOException {
long operationStartTimeMs = clock.uptimeMillis();
// Remove other pending DO_SOME_WORK requests that are handled by this invocation.
handler.removeMessages(MSG_DO_SOME_WORK);
updatePeriods();
if (playbackInfo.playbackState == Player.STATE_IDLE
|| playbackInfo.playbackState == Player.STATE_ENDED) {
// Remove all messages. Prepare (in case of IDLE) or seek (in case of ENDED) will resume.
handler.removeMessages(MSG_DO_SOME_WORK);
// Nothing to do. Prepare (in case of IDLE) or seek (in case of ENDED) will resume.
return;
}
@ -1078,24 +1078,24 @@ import java.util.concurrent.atomic.AtomicBoolean;
throw new IllegalStateException("Playback stuck buffering and not loading");
}
if (offloadSchedulingEnabled != playbackInfo.offloadSchedulingEnabled) {
playbackInfo = playbackInfo.copyWithOffloadSchedulingEnabled(offloadSchedulingEnabled);
}
boolean sleepingForOffload = false;
if ((shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY)
|| playbackInfo.playbackState == Player.STATE_BUFFERING) {
sleepingForOffload = !maybeScheduleWakeup(operationStartTimeMs, ACTIVE_INTERVAL_MS);
} else if (enabledRendererCount != 0 && playbackInfo.playbackState != Player.STATE_ENDED) {
scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
} else {
handler.removeMessages(MSG_DO_SOME_WORK);
}
boolean isPlaying = shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY;
boolean sleepingForOffload = offloadSchedulingEnabled && requestForRendererSleep && isPlaying;
if (playbackInfo.sleepingForOffload != sleepingForOffload) {
playbackInfo = playbackInfo.copyWithSleepingForOffload(sleepingForOffload);
}
requestForRendererSleep = false; // A sleep request is only valid for the current doSomeWork.
if (sleepingForOffload || playbackInfo.playbackState == Player.STATE_ENDED) {
// No need to schedule next work.
return;
} else if (isPlaying || playbackInfo.playbackState == Player.STATE_BUFFERING) {
// We are actively playing or waiting for data to be ready. Schedule next work quickly.
scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
} else if (playbackInfo.playbackState == Player.STATE_READY && enabledRendererCount != 0) {
// We are ready, but not playing. Schedule next work less often to handle non-urgent updates.
scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
}
TraceUtil.endSection();
}
@ -1125,19 +1125,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {
handler.removeMessages(MSG_DO_SOME_WORK);
handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs);
}
private boolean maybeScheduleWakeup(long operationStartTimeMs, long intervalMs) {
if (offloadSchedulingEnabled && requestForRendererSleep) {
return false;
}
scheduleNextWork(operationStartTimeMs, intervalMs);
return true;
}
private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
@ -1468,7 +1458,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* bufferedPositionUs= */ startPositionUs,
/* totalBufferedDurationUs= */ 0,
/* positionUs= */ startPositionUs,
offloadSchedulingEnabled,
/* sleepingForOffload= */ false);
if (releaseMediaSourceList) {
mediaSourceList.release();

View File

@ -74,8 +74,6 @@ import java.util.List;
public final @PlaybackSuppressionReason int playbackSuppressionReason;
/** The playback parameters. */
public final PlaybackParameters playbackParameters;
/** Whether offload scheduling is enabled for the main player loop. */
public final boolean offloadSchedulingEnabled;
/** Whether the main player loop is sleeping, while using offload scheduling. */
public final boolean sleepingForOffload;
@ -122,7 +120,6 @@ import java.util.List;
/* bufferedPositionUs= */ 0,
/* totalBufferedDurationUs= */ 0,
/* positionUs= */ 0,
/* offloadSchedulingEnabled= */ false,
/* sleepingForOffload= */ false);
}
@ -145,7 +142,6 @@ import java.util.List;
* @param bufferedPositionUs See {@link #bufferedPositionUs}.
* @param totalBufferedDurationUs See {@link #totalBufferedDurationUs}.
* @param positionUs See {@link #positionUs}.
* @param offloadSchedulingEnabled See {@link #offloadSchedulingEnabled}.
* @param sleepingForOffload See {@link #sleepingForOffload}.
*/
public PlaybackInfo(
@ -166,7 +162,6 @@ import java.util.List;
long bufferedPositionUs,
long totalBufferedDurationUs,
long positionUs,
boolean offloadSchedulingEnabled,
boolean sleepingForOffload) {
this.timeline = timeline;
this.periodId = periodId;
@ -185,7 +180,6 @@ import java.util.List;
this.bufferedPositionUs = bufferedPositionUs;
this.totalBufferedDurationUs = totalBufferedDurationUs;
this.positionUs = positionUs;
this.offloadSchedulingEnabled = offloadSchedulingEnabled;
this.sleepingForOffload = sleepingForOffload;
}
@ -237,7 +231,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
@ -267,7 +260,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
@ -297,7 +289,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
@ -327,7 +318,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
@ -357,7 +347,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
@ -387,7 +376,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
@ -421,7 +409,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
@ -451,38 +438,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
/**
* Copies playback info with new offloadSchedulingEnabled.
*
* @param offloadSchedulingEnabled New offloadSchedulingEnabled state. See {@link
* #offloadSchedulingEnabled}.
* @return Copied playback info with new offload scheduling state.
*/
@CheckResult
public PlaybackInfo copyWithOffloadSchedulingEnabled(boolean offloadSchedulingEnabled) {
return new PlaybackInfo(
timeline,
periodId,
requestedContentPositionUs,
discontinuityStartPositionUs,
playbackState,
playbackError,
isLoading,
trackGroups,
trackSelectorResult,
staticMetadata,
loadingMediaPeriodId,
playWhenReady,
playbackSuppressionReason,
playbackParameters,
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
@ -512,7 +467,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
}

View File

@ -29,7 +29,6 @@ import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.PlaybackParams;
import android.media.metrics.LogSessionId;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Pair;
@ -44,6 +43,8 @@ import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
@ -615,7 +616,8 @@ public final class DefaultAudioSink implements AudioSink {
enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && builder.enableAudioTrackPlaybackParams;
offloadMode = Util.SDK_INT >= 29 ? builder.offloadMode : OFFLOAD_MODE_DISABLED;
audioTrackBufferSizeProvider = builder.audioTrackBufferSizeProvider;
releasingConditionVariable = new ConditionVariable(true);
releasingConditionVariable = new ConditionVariable(Clock.DEFAULT);
releasingConditionVariable.open();
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
trimmingAudioProcessor = new TrimmingAudioProcessor();
@ -840,13 +842,15 @@ public final class DefaultAudioSink implements AudioSink {
}
}
private void initializeAudioTrack() throws InitializationException {
// If we're asynchronously releasing a previous audio track then we block until it has been
private boolean initializeAudioTrack() throws InitializationException {
// If we're asynchronously releasing a previous audio track then we wait until it has been
// released. This guarantees that we cannot end up in a state where we have multiple audio
// track instances. Without this guarantee it would be possible, in extreme cases, to exhaust
// the shared memory that's available for audio track buffers. This would in turn cause the
// initialization of the audio track to fail.
releasingConditionVariable.block();
if (!releasingConditionVariable.isOpen()) {
return false;
}
audioTrack = buildAudioTrackWithRetry();
if (isOffloadedPlayback(audioTrack)) {
@ -874,6 +878,7 @@ public final class DefaultAudioSink implements AudioSink {
}
startMediaTimeUsNeedsInit = true;
return true;
}
@Override
@ -930,7 +935,10 @@ public final class DefaultAudioSink implements AudioSink {
if (!isAudioTrackInitialized()) {
try {
initializeAudioTrack();
if (!initializeAudioTrack()) {
// Not yet ready for initialization of a new AudioTrack.
return false;
}
} catch (InitializationException e) {
if (e.isRecoverable) {
throw e; // Do not delay the exception if it can be recovered at higher level.

View File

@ -317,7 +317,9 @@ public final class MediaCodecInfo {
}
for (CodecProfileLevel profileLevel : profileLevels) {
if (profileLevel.profile == profile && profileLevel.level >= level) {
if (profileLevel.profile == profile
&& profileLevel.level >= level
&& !needsProfileExcludedWorkaround(mimeType, profile)) {
return true;
}
}
@ -831,4 +833,15 @@ public final class MediaCodecInfo {
}
return true;
}
/**
* Whether a profile is excluded from the list of supported profiles. This may happen when a
* device declares support for a profile it doesn't actually support.
*/
private static boolean needsProfileExcludedWorkaround(String mimeType, int profile) {
// See https://github.com/google/ExoPlayer/issues/3537
return MimeTypes.VIDEO_H265.equals(mimeType)
&& CodecProfileLevel.HEVCProfileMain10 == profile
&& ("sailfish".equals(Util.DEVICE) || "marlin".equals(Util.DEVICE));
}
}

View File

@ -282,6 +282,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
*/
public DefaultMediaSourceFactory setDataSourceFactory(DataSource.Factory dataSourceFactory) {
this.dataSourceFactory = dataSourceFactory;
delegateFactoryLoader.setDataSourceFactory(dataSourceFactory);
return this;
}
@ -594,6 +595,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
this.dataSourceFactory = dataSourceFactory;
// TODO(b/233577470): Call MediaSource.Factory.setDataSourceFactory on each value when it
// exists on the interface.
mediaSourceFactorySuppliers.clear();
mediaSourceFactories.clear();
}
}
@ -627,6 +629,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
}
@Nullable Supplier<MediaSource.Factory> mediaSourceFactorySupplier = null;
DataSource.Factory dataSourceFactory = checkNotNull(this.dataSourceFactory);
try {
Class<? extends MediaSource.Factory> clazz;
switch (contentType) {
@ -634,19 +637,19 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
clazz =
Class.forName("androidx.media3.exoplayer.dash.DashMediaSource$Factory")
.asSubclass(MediaSource.Factory.class);
mediaSourceFactorySupplier = () -> newInstance(clazz, checkNotNull(dataSourceFactory));
mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
break;
case C.CONTENT_TYPE_SS:
clazz =
Class.forName("androidx.media3.exoplayer.smoothstreaming.SsMediaSource$Factory")
.asSubclass(MediaSource.Factory.class);
mediaSourceFactorySupplier = () -> newInstance(clazz, checkNotNull(dataSourceFactory));
mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
break;
case C.CONTENT_TYPE_HLS:
clazz =
Class.forName("androidx.media3.exoplayer.hls.HlsMediaSource$Factory")
.asSubclass(MediaSource.Factory.class);
mediaSourceFactorySupplier = () -> newInstance(clazz, checkNotNull(dataSourceFactory));
mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
break;
case C.CONTENT_TYPE_RTSP:
clazz =
@ -656,9 +659,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
break;
case C.CONTENT_TYPE_OTHER:
mediaSourceFactorySupplier =
() ->
new ProgressiveMediaSource.Factory(
checkNotNull(dataSourceFactory), extractorsFactory);
() -> new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory);
break;
default:
// Do nothing.

View File

@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer.source;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.lang.Math.max;
import static java.lang.Math.min;
@ -135,7 +136,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private boolean seenFirstTrackSelection;
private boolean notifyDiscontinuity;
private int enabledTrackCount;
private long length;
private boolean isLengthKnown;
private long lastSeekPositionUs;
private long pendingResetPositionUs;
@ -193,15 +194,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
onContinueLoadingRequestedRunnable =
() -> {
if (!released) {
Assertions.checkNotNull(callback)
.onContinueLoadingRequested(ProgressiveMediaPeriod.this);
checkNotNull(callback).onContinueLoadingRequested(ProgressiveMediaPeriod.this);
}
};
handler = Util.createHandlerForCurrentLooper();
sampleQueueTrackIds = new TrackId[0];
sampleQueues = new SampleQueue[0];
pendingResetPositionUs = C.TIME_UNSET;
length = C.LENGTH_UNSET;
durationUs = C.TIME_UNSET;
dataType = C.DATA_TYPE_MEDIA;
}
@ -367,7 +366,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override
public long getNextLoadPositionUs() {
return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs();
return getBufferedPositionUs();
}
@Override
@ -383,8 +382,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override
public long getBufferedPositionUs() {
assertPrepared();
boolean[] trackIsAudioVideoFlags = trackState.trackIsAudioVideoFlags;
if (loadingFinished) {
if (loadingFinished || enabledTrackCount == 0) {
return C.TIME_END_OF_SOURCE;
} else if (isPendingReset()) {
return pendingResetPositionUs;
@ -394,14 +392,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Ignore non-AV tracks, which may be sparse or poorly interleaved.
int trackCount = sampleQueues.length;
for (int i = 0; i < trackCount; i++) {
if (trackIsAudioVideoFlags[i] && !sampleQueues[i].isLastSampleQueued()) {
if (trackState.trackIsAudioVideoFlags[i]
&& trackState.trackEnabledStates[i]
&& !sampleQueues[i].isLastSampleQueued()) {
largestQueuedTimestampUs =
min(largestQueuedTimestampUs, sampleQueues[i].getLargestQueuedTimestampUs());
}
}
}
if (largestQueuedTimestampUs == Long.MAX_VALUE) {
largestQueuedTimestampUs = getLargestQueuedTimestampUs();
largestQueuedTimestampUs = getLargestQueuedTimestampUs(/* includeDisabledTracks= */ false);
}
return largestQueuedTimestampUs == Long.MIN_VALUE
? lastSeekPositionUs
@ -537,7 +537,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.reset();
}
Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
checkNotNull(callback).onContinueLoadingRequested(this);
}
private boolean suppressRead() {
@ -551,7 +551,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) {
if (durationUs == C.TIME_UNSET && seekMap != null) {
boolean isSeekable = seekMap.isSeekable();
long largestQueuedTimestampUs = getLargestQueuedTimestampUs();
long largestQueuedTimestampUs =
getLargestQueuedTimestampUs(/* includeDisabledTracks= */ true);
durationUs =
largestQueuedTimestampUs == Long.MIN_VALUE
? 0
@ -578,9 +579,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ loadable.seekTimeUs,
durationUs);
copyLengthFromLoader(loadable);
loadingFinished = true;
Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
checkNotNull(callback).onContinueLoadingRequested(this);
}
@Override
@ -607,12 +607,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* mediaStartTimeUs= */ loadable.seekTimeUs,
durationUs);
if (!released) {
copyLengthFromLoader(loadable);
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.reset();
}
if (enabledTrackCount > 0) {
Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
checkNotNull(callback).onContinueLoadingRequested(this);
}
}
}
@ -624,7 +623,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
long loadDurationMs,
IOException error,
int errorCount) {
copyLengthFromLoader(loadable);
StatsDataSource dataSource = loadable.dataSource;
LoadEventInfo loadEventInfo =
new LoadEventInfo(
@ -710,6 +708,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Internal methods.
private void onLengthKnown() {
handler.post(() -> isLengthKnown = true);
}
private TrackOutput prepareTrackOutput(TrackId id) {
int trackCount = sampleQueues.length;
for (int i = 0; i < trackCount; i++) {
@ -733,7 +735,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private void setSeekMap(SeekMap seekMap) {
this.seekMap = icyHeaders == null ? seekMap : new Unseekable(/* durationUs= */ C.TIME_UNSET);
durationUs = seekMap.getDurationUs();
isLive = length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET;
isLive = !isLengthKnown && seekMap.getDurationUs() == C.TIME_UNSET;
dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA;
listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive);
if (!prepared) {
@ -755,7 +757,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
TrackGroup[] trackArray = new TrackGroup[trackCount];
boolean[] trackIsAudioVideoFlags = new boolean[trackCount];
for (int i = 0; i < trackCount; i++) {
Format trackFormat = Assertions.checkNotNull(sampleQueues[i].getUpstreamFormat());
Format trackFormat = checkNotNull(sampleQueues[i].getUpstreamFormat());
@Nullable String mimeType = trackFormat.sampleMimeType;
boolean isAudio = MimeTypes.isAudio(mimeType);
boolean isAudioVideo = isAudio || MimeTypes.isVideo(mimeType);
@ -786,13 +788,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
trackState = new TrackState(new TrackGroupArray(trackArray), trackIsAudioVideoFlags);
prepared = true;
Assertions.checkNotNull(callback).onPrepared(this);
}
private void copyLengthFromLoader(ExtractingLoadable loadable) {
if (length == C.LENGTH_UNSET) {
length = loadable.length;
}
checkNotNull(callback).onPrepared(this);
}
private void startLoading() {
@ -807,7 +803,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return;
}
loadable.setLoadPosition(
Assertions.checkNotNull(seekMap).getSeekPoints(pendingResetPositionUs).first.position,
checkNotNull(seekMap).getSeekPoints(pendingResetPositionUs).first.position,
pendingResetPositionUs);
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.setStartTimeUs(pendingResetPositionUs);
@ -840,7 +836,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* retry.
*/
private boolean configureRetry(ExtractingLoadable loadable, int currentExtractedSampleCount) {
if (length != C.LENGTH_UNSET || (seekMap != null && seekMap.getDurationUs() != C.TIME_UNSET)) {
if (isLengthKnown || (seekMap != null && seekMap.getDurationUs() != C.TIME_UNSET)) {
// We're playing an on-demand stream. Resume the current loadable, which will
// request data starting from the point it left off.
extractedSamplesCountAtStartOfLoad = currentExtractedSampleCount;
@ -904,11 +900,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return extractedSamplesCount;
}
private long getLargestQueuedTimestampUs() {
private long getLargestQueuedTimestampUs(boolean includeDisabledTracks) {
long largestQueuedTimestampUs = Long.MIN_VALUE;
for (SampleQueue sampleQueue : sampleQueues) {
largestQueuedTimestampUs =
max(largestQueuedTimestampUs, sampleQueue.getLargestQueuedTimestampUs());
for (int i = 0; i < sampleQueues.length; i++) {
if (includeDisabledTracks || checkNotNull(trackState).trackEnabledStates[i]) {
largestQueuedTimestampUs =
max(largestQueuedTimestampUs, sampleQueues[i].getLargestQueuedTimestampUs());
}
}
return largestQueuedTimestampUs;
}
@ -920,8 +918,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@EnsuresNonNull({"trackState", "seekMap"})
private void assertPrepared() {
Assertions.checkState(prepared);
Assertions.checkNotNull(trackState);
Assertions.checkNotNull(seekMap);
checkNotNull(trackState);
checkNotNull(seekMap);
}
private final class SampleStreamImpl implements SampleStream {
@ -970,7 +968,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private boolean pendingExtractorSeek;
private long seekTimeUs;
private DataSpec dataSpec;
private long length;
@Nullable private TrackOutput icyTrackOutput;
private boolean seenIcyMetadata;
@ -988,7 +985,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.loadCondition = loadCondition;
this.positionHolder = new PositionHolder();
this.pendingExtractorSeek = true;
this.length = C.LENGTH_UNSET;
loadTaskId = LoadEventInfo.getNewId();
dataSpec = buildDataSpec(/* position= */ 0);
}
@ -1007,9 +1003,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
try {
long position = positionHolder.position;
dataSpec = buildDataSpec(position);
length = dataSource.open(dataSpec);
long length = dataSource.open(dataSpec);
if (length != C.LENGTH_UNSET) {
length += position;
onLengthKnown();
}
icyHeaders = IcyHeaders.parse(dataSource.getResponseHeaders());
DataSource extractorDataSource = dataSource;
@ -1065,9 +1062,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void onIcyMetadata(ParsableByteArray metadata) {
// Always output the first ICY metadata at the start time. This helps minimize any delay
// between the start of playback and the first ICY metadata event.
long timeUs = !seenIcyMetadata ? seekTimeUs : max(getLargestQueuedTimestampUs(), seekTimeUs);
long timeUs =
!seenIcyMetadata
? seekTimeUs
: max(getLargestQueuedTimestampUs(/* includeDisabledTracks= */ true), seekTimeUs);
int length = metadata.bytesLeft();
TrackOutput icyTrackOutput = Assertions.checkNotNull(this.icyTrackOutput);
TrackOutput icyTrackOutput = checkNotNull(this.icyTrackOutput);
icyTrackOutput.sampleData(metadata, length);
icyTrackOutput.sampleMetadata(
timeUs, C.BUFFER_FLAG_KEY_FRAME, length, /* offset= */ 0, /* cryptoData= */ null);

View File

@ -39,7 +39,7 @@ public interface TextOutput {
* Called when there is a change in the {@link CueGroup}.
*
* <p>Both {@link #onCues(List)} and {@link #onCues(CueGroup)} are called when there is a change
* in the cues You should only implement one or the other.
* in the cues. You should only implement one or the other.
*/
void onCues(CueGroup cueGroup);
}

View File

@ -629,7 +629,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
surface = placeholderSurface;
} else {
MediaCodecInfo codecInfo = getCodecInfo();
if (codecInfo != null && shouldUseDummySurface(codecInfo)) {
if (codecInfo != null && shouldUsePlaceholderSurface(codecInfo)) {
placeholderSurface = PlaceholderSurface.newInstanceV17(context, codecInfo.secure);
surface = placeholderSurface;
}
@ -675,7 +675,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override
protected boolean shouldInitCodec(MediaCodecInfo codecInfo) {
return surface != null || shouldUseDummySurface(codecInfo);
return surface != null || shouldUsePlaceholderSurface(codecInfo);
}
@Override
@ -706,7 +706,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
deviceNeedsNoPostProcessWorkaround,
tunneling ? tunnelingAudioSessionId : C.AUDIO_SESSION_ID_UNSET);
if (surface == null) {
if (!shouldUseDummySurface(codecInfo)) {
if (!shouldUsePlaceholderSurface(codecInfo)) {
throw new IllegalStateException();
}
if (placeholderSurface == null) {
@ -1333,7 +1333,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
maybeNotifyRenderedFirstFrame();
}
private boolean shouldUseDummySurface(MediaCodecInfo codecInfo) {
private boolean shouldUsePlaceholderSurface(MediaCodecInfo codecInfo) {
return Util.SDK_INT >= 23
&& !tunneling
&& !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name)
@ -1572,7 +1572,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
if (haveUnknownDimensions) {
Log.w(TAG, "Resolutions unknown. Codec max resolution: " + maxWidth + "x" + maxHeight);
Point codecMaxSize = getCodecMaxSize(codecInfo, format);
@Nullable Point codecMaxSize = getCodecMaxSize(codecInfo, format);
if (codecMaxSize != null) {
maxWidth = max(maxWidth, codecMaxSize.x);
maxHeight = max(maxHeight, codecMaxSize.y);
@ -1600,8 +1600,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
*
* @param codecInfo Information about the {@link MediaCodec} being configured.
* @param format The {@link Format} for which the codec is being configured.
* @return The maximum video size to use, or null if the size of {@code format} should be used.
* @return The maximum video size to use, or {@code null} if the size of {@code format} should be
* used.
*/
@Nullable
private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) {
boolean isVerticalVideo = format.height > format.width;
int formatLongEdgePx = isVerticalVideo ? format.height : format.width;

View File

@ -53,13 +53,13 @@ import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.o
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
import static androidx.media3.test.utils.TestUtil.assertTimelinesSame;
import static androidx.media3.test.utils.TestUtil.timelinesAreSame;
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilPosition;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilStartOfMediaItem;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPositionDiscontinuity;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilReceiveOffloadSchedulingEnabledNewState;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilSleepingForOffload;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilTimelineChanged;
import static com.google.common.truth.Truth.assertThat;
@ -125,6 +125,7 @@ import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.MediaSourceEventListener;
import androidx.media3.exoplayer.source.ShuffleOrder;
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource;
@ -157,7 +158,6 @@ import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
import androidx.media3.test.utils.FakeTrackSelection;
import androidx.media3.test.utils.FakeTrackSelector;
import androidx.media3.test.utils.FakeVideoRenderer;
import androidx.media3.test.utils.NoUidTimeline;
import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.core.app.ApplicationProvider;
@ -6512,6 +6512,53 @@ public final class ExoPlayerTest {
assertThat(positionAfterSetShuffleOrder.get()).isAtLeast(5000);
}
@Test
public void setShuffleOrder_notifiesTimelineChanged() throws Exception {
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
// No callback expected for this call, because the (empty) timeline doesn't change. We start
// with a deterministic shuffle order, to ensure when we call setShuffleOrder again below the
// order is definitely different (otherwise the test is flaky when the existing shuffle order
// matches the shuffle order passed in below).
player.setShuffleOrder(new FakeShuffleOrder(0));
player.setMediaSources(
ImmutableList.of(new FakeMediaSource(), new FakeMediaSource(), new FakeMediaSource()));
Player.Listener mockListener = mock(Player.Listener.class);
player.addListener(mockListener);
player.prepare();
TestPlayerRunHelper.playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 5000);
player.play();
ShuffleOrder.DefaultShuffleOrder newShuffleOrder =
new ShuffleOrder.DefaultShuffleOrder(player.getMediaItemCount(), /* randomSeed= */ 5);
player.setShuffleOrder(newShuffleOrder);
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
ArgumentCaptor<Timeline> timelineCaptor = ArgumentCaptor.forClass(Timeline.class);
verify(mockListener)
.onTimelineChanged(
timelineCaptor.capture(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
Timeline capturedTimeline = Iterables.getOnlyElement(timelineCaptor.getAllValues());
List<Integer> newShuffleOrderIndexes = new ArrayList<>(newShuffleOrder.getLength());
for (int i = newShuffleOrder.getFirstIndex();
i != C.INDEX_UNSET;
i = newShuffleOrder.getNextIndex(i)) {
newShuffleOrderIndexes.add(i);
}
List<Integer> capturedTimelineShuffleIndexes = new ArrayList<>(newShuffleOrder.getLength());
for (int i = capturedTimeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true);
i != C.INDEX_UNSET;
i =
capturedTimeline.getNextWindowIndex(
i, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) {
capturedTimelineShuffleIndexes.add(i);
}
assertThat(capturedTimelineShuffleIndexes).isEqualTo(newShuffleOrderIndexes);
}
@Test
public void setMediaSources_empty_whenEmpty_correctMaskingMediaItemIndex() throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
@ -9635,47 +9682,16 @@ public final class ExoPlayerTest {
}
@Test
public void enableOffloadSchedulingWhileIdle_isToggled_isReported() throws Exception {
public void enableOffloadScheduling_isReported() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
ExoPlayer.AudioOffloadListener mockListener = mock(ExoPlayer.AudioOffloadListener.class);
player.addAudioOffloadListener(mockListener);
player.experimentalSetOffloadSchedulingEnabled(true);
assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isTrue();
verify(mockListener).onExperimentalOffloadSchedulingEnabledChanged(true);
player.experimentalSetOffloadSchedulingEnabled(false);
assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse();
}
@Test
public void enableOffloadSchedulingWhilePlaying_isToggled_isReported() throws Exception {
FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO).sleepOnNextRender();
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(sleepRenderer).build();
Timeline timeline = new FakeTimeline();
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT));
player.prepare();
player.play();
player.experimentalSetOffloadSchedulingEnabled(true);
assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isTrue();
player.experimentalSetOffloadSchedulingEnabled(false);
assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse();
}
@Test
public void enableOffloadSchedulingWhileSleepingForOffload_isDisabled_isReported()
throws Exception {
FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO).sleepOnNextRender();
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(sleepRenderer).build();
Timeline timeline = new FakeTimeline();
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT));
player.experimentalSetOffloadSchedulingEnabled(true);
player.prepare();
player.play();
runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true);
player.experimentalSetOffloadSchedulingEnabled(false);
assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse();
verify(mockListener).onExperimentalOffloadSchedulingEnabledChanged(false);
}
@Test
@ -12296,6 +12312,6 @@ public final class ExoPlayerTest {
* Returns an argument matcher for {@link Timeline} instances that ignores period and window uids.
*/
private static ArgumentMatcher<Timeline> noUid(Timeline timeline) {
return argument -> new NoUidTimeline(timeline).equals(new NoUidTimeline(argument));
return argument -> timelinesAreSame(argument, timeline);
}
}

View File

@ -1112,7 +1112,6 @@ public final class MediaPeriodQueueTest {
/* bufferedPositionUs= */ 0,
/* totalBufferedDurationUs= */ 0,
/* positionUs= */ 0,
/* offloadSchedulingEnabled= */ false,
/* sleepingForOffload= */ false);
}

View File

@ -57,6 +57,7 @@ public class Mp4PlaybackTest {
"sample_eac3joc.mp4",
"sample_fragmented.mp4",
"sample_fragmented_seekable.mp4",
"sample_fragmented_large_bitrates.mp4",
"sample_fragmented_sei.mp4",
"sample_mdat_too_long.mp4",
"sample.mp4",

View File

@ -599,6 +599,9 @@ public class DashManifestParser extends DefaultHandler
case "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed":
uuid = C.WIDEVINE_UUID;
break;
case "urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e":
uuid = C.CLEARKEY_UUID;
break;
default:
break;
}
@ -606,7 +609,9 @@ public class DashManifestParser extends DefaultHandler
do {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) {
if (XmlPullParserUtil.isStartTag(xpp, "clearkey:Laurl") && xpp.next() == XmlPullParser.TEXT) {
licenseServerUrl = xpp.getText();
} else if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) {
licenseServerUrl = xpp.getAttributeValue(null, "licenseUrl");
} else if (data == null
&& XmlPullParserUtil.isStartTagIgnorePrefix(xpp, "pssh")
@ -853,6 +858,7 @@ public class DashManifestParser extends DefaultHandler
ArrayList<SchemeData> drmSchemeDatas = representationInfo.drmSchemeDatas;
drmSchemeDatas.addAll(extraDrmSchemeDatas);
if (!drmSchemeDatas.isEmpty()) {
fillInClearKeyInformation(drmSchemeDatas);
filterRedundantIncompleteSchemeDatas(drmSchemeDatas);
formatBuilder.setDrmInitData(new DrmInitData(drmSchemeType, drmSchemeDatas));
}
@ -1660,6 +1666,32 @@ public class DashManifestParser extends DefaultHandler
}
}
private static void fillInClearKeyInformation(ArrayList<SchemeData> schemeDatas) {
// Find and remove ClearKey information.
@Nullable String clearKeyLicenseServerUrl = null;
for (int i = 0; i < schemeDatas.size(); i++) {
SchemeData schemeData = schemeDatas.get(i);
if (C.CLEARKEY_UUID.equals(schemeData.uuid) && schemeData.licenseServerUrl != null) {
clearKeyLicenseServerUrl = schemeData.licenseServerUrl;
schemeDatas.remove(i);
break;
}
}
if (clearKeyLicenseServerUrl == null) {
return;
}
// Fill in the ClearKey information into the existing PSSH schema data if applicable.
for (int i = 0; i < schemeDatas.size(); i++) {
SchemeData schemeData = schemeDatas.get(i);
if (C.COMMON_PSSH_UUID.equals(schemeData.uuid) && schemeData.licenseServerUrl == null) {
schemeDatas.set(
i,
new SchemeData(
C.CLEARKEY_UUID, clearKeyLicenseServerUrl, schemeData.mimeType, schemeData.data));
}
}
}
/**
* Derives a sample mimeType from a container mimeType and codecs attribute.
*

View File

@ -15,16 +15,21 @@
*/
package androidx.media3.exoplayer.dash;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.test.utils.FakeDataSource;
import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -82,4 +87,53 @@ public class DefaultMediaSourceFactoryTest {
assertThat(supportedTypes).asList().containsExactly(C.CONTENT_TYPE_OTHER, C.CONTENT_TYPE_DASH);
}
@Test
public void createMediaSource_withSetDataSourceFactory_usesDataSourceFactory() throws Exception {
FakeDataSource fakeDataSource = new FakeDataSource();
DefaultMediaSourceFactory defaultMediaSourceFactory =
new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext())
.setDataSourceFactory(() -> fakeDataSource);
prepareDashUrlAndWaitForPrepareError(defaultMediaSourceFactory);
assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
}
@Test
public void
createMediaSource_usingDefaultDataSourceFactoryAndSetDataSourceFactory_usesUpdatesDataSourceFactory()
throws Exception {
FakeDataSource fakeDataSource = new FakeDataSource();
DefaultMediaSourceFactory defaultMediaSourceFactory =
new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext());
// Use default DataSource.Factory first.
prepareDashUrlAndWaitForPrepareError(defaultMediaSourceFactory);
defaultMediaSourceFactory.setDataSourceFactory(() -> fakeDataSource);
prepareDashUrlAndWaitForPrepareError(defaultMediaSourceFactory);
assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
}
private static void prepareDashUrlAndWaitForPrepareError(
DefaultMediaSourceFactory defaultMediaSourceFactory) throws Exception {
MediaSource mediaSource =
defaultMediaSourceFactory.createMediaSource(MediaItem.fromUri(URI_MEDIA + "/file.mpd"));
getInstrumentation()
.runOnMainSync(
() ->
mediaSource.prepareSource(
(source, timeline) -> {}, /* mediaTransferListener= */ null, PlayerId.UNSET));
// We don't expect this to prepare successfully.
RobolectricUtil.runMainLooperUntil(
() -> {
try {
mediaSource.maybeThrowSourceInfoRefreshError();
return false;
} catch (IOException e) {
return true;
}
});
}
}

View File

@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.DrmInitData;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util;
@ -79,6 +80,8 @@ public class DashManifestParserTest {
"media/mpd/sample_mpd_service_description_low_latency_only_playback_rates";
private static final String SAMPLE_MPD_SERVICE_DESCRIPTION_LOW_LATENCY_ONLY_TARGET_LATENCY =
"media/mpd/sample_mpd_service_description_low_latency_only_target_latency";
private static final String SAMPLE_MPD_CLEAR_KEY_LICENSE_URL =
"media/mpd/sample_mpd_clear_key_license_url";
private static final String NEXT_TAG_NAME = "Next";
private static final String NEXT_TAG = "<" + NEXT_TAG_NAME + "/>";
@ -880,6 +883,37 @@ public class DashManifestParserTest {
assertThat(manifest.serviceDescription).isNull();
}
@Test
public void contentProtections_withClearKeyLicenseUrl() throws IOException {
DashManifestParser parser = new DashManifestParser();
DashManifest manifest =
parser.parse(
Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream(
ApplicationProvider.getApplicationContext(), SAMPLE_MPD_CLEAR_KEY_LICENSE_URL));
assertThat(manifest.getPeriodCount()).isEqualTo(1);
Period period = manifest.getPeriod(0);
assertThat(period.adaptationSets).hasSize(2);
AdaptationSet adaptationSet0 = period.adaptationSets.get(0);
AdaptationSet adaptationSet1 = period.adaptationSets.get(1);
assertThat(adaptationSet0.representations).hasSize(1);
assertThat(adaptationSet1.representations).hasSize(1);
Representation representation0 = adaptationSet0.representations.get(0);
Representation representation1 = adaptationSet1.representations.get(0);
assertThat(representation0.format.drmInitData.schemeType).isEqualTo("cenc");
assertThat(representation1.format.drmInitData.schemeType).isEqualTo("cenc");
assertThat(representation0.format.drmInitData.schemeDataCount).isEqualTo(1);
assertThat(representation1.format.drmInitData.schemeDataCount).isEqualTo(1);
DrmInitData.SchemeData schemeData0 = representation0.format.drmInitData.get(0);
DrmInitData.SchemeData schemeData1 = representation1.format.drmInitData.get(0);
assertThat(schemeData0.uuid).isEqualTo(C.CLEARKEY_UUID);
assertThat(schemeData1.uuid).isEqualTo(C.CLEARKEY_UUID);
assertThat(schemeData0.licenseServerUrl).isEqualTo("https://testserver1.test/AcquireLicense");
assertThat(schemeData1.licenseServerUrl).isEqualTo("https://testserver2.test/AcquireLicense");
}
private static List<Descriptor> buildCea608AccessibilityDescriptors(String value) {
return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-608:2015", value, null));
}

View File

@ -15,16 +15,21 @@
*/
package androidx.media3.exoplayer.hls;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.test.utils.FakeDataSource;
import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -82,4 +87,53 @@ public class DefaultMediaSourceFactoryTest {
assertThat(supportedTypes).asList().containsExactly(C.CONTENT_TYPE_OTHER, C.CONTENT_TYPE_HLS);
}
@Test
public void createMediaSource_withSetDataSourceFactory_usesDataSourceFactory() throws Exception {
FakeDataSource fakeDataSource = new FakeDataSource();
DefaultMediaSourceFactory defaultMediaSourceFactory =
new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext())
.setDataSourceFactory(() -> fakeDataSource);
prepareHlsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
}
@Test
public void
createMediaSource_usingDefaultDataSourceFactoryAndSetDataSourceFactory_usesUpdatesDataSourceFactory()
throws Exception {
FakeDataSource fakeDataSource = new FakeDataSource();
DefaultMediaSourceFactory defaultMediaSourceFactory =
new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext());
// Use default DataSource.Factory first.
prepareHlsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
defaultMediaSourceFactory.setDataSourceFactory(() -> fakeDataSource);
prepareHlsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
}
private static void prepareHlsUrlAndWaitForPrepareError(
DefaultMediaSourceFactory defaultMediaSourceFactory) throws Exception {
MediaSource mediaSource =
defaultMediaSourceFactory.createMediaSource(MediaItem.fromUri(URI_MEDIA + "/file.m3u8"));
getInstrumentation()
.runOnMainSync(
() ->
mediaSource.prepareSource(
(source, timeline) -> {}, /* mediaTransferListener= */ null, PlayerId.UNSET));
// We don't expect this to prepare successfully.
RobolectricUtil.runMainLooperUntil(
() -> {
try {
mediaSource.maybeThrowSourceInfoRefreshError();
return false;
} catch (IOException e) {
return true;
}
});
}
}

View File

@ -15,6 +15,8 @@
*/
package androidx.media3.exoplayer.rtsp.reader;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import androidx.media3.common.C;
@ -51,6 +53,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** The combined size of a sample that is fragmented into multiple RTP packets. */
private int fragmentedSampleSizeBytes;
private long fragmentedSampleTimeUs;
private long startTimeOffsetUs;
/**
* Whether the first packet of one VP8 frame is received. A VP8 frame can be split into two RTP
@ -67,6 +71,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
firstReceivedTimestamp = C.TIME_UNSET;
previousSequenceNumber = C.INDEX_UNSET;
fragmentedSampleSizeBytes = C.LENGTH_UNSET;
fragmentedSampleTimeUs = C.TIME_UNSET;
// The start time offset must be 0 until the first seek.
startTimeOffsetUs = 0;
gotFirstPacketOfVp8Frame = false;
@ -81,7 +86,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
@Override
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {}
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {
checkState(firstReceivedTimestamp == C.TIME_UNSET);
firstReceivedTimestamp = timestamp;
}
@Override
public void consume(
@ -113,21 +121,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
int fragmentSize = data.bytesLeft();
trackOutput.sampleData(data, fragmentSize);
fragmentedSampleSizeBytes += fragmentSize;
if (fragmentedSampleSizeBytes == C.LENGTH_UNSET) {
fragmentedSampleSizeBytes = fragmentSize;
} else {
fragmentedSampleSizeBytes += fragmentSize;
}
fragmentedSampleTimeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
if (rtpMarker) {
if (firstReceivedTimestamp == C.TIME_UNSET) {
firstReceivedTimestamp = timestamp;
}
long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
trackOutput.sampleMetadata(
timeUs,
isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
fragmentedSampleSizeBytes,
/* offset= */ 0,
/* cryptoData= */ null);
fragmentedSampleSizeBytes = C.LENGTH_UNSET;
gotFirstPacketOfVp8Frame = false;
outputSampleMetadataForFragmentedPackets();
}
previousSequenceNumber = sequenceNumber;
}
@ -147,18 +150,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSequenceNumber) {
// VP8 Payload Descriptor is defined in RFC7741 Section 4.2.
int header = payload.readUnsignedByte();
if (!gotFirstPacketOfVp8Frame) {
// TODO(b/198620566) Consider using ParsableBitArray.
// For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2.
if ((header & 0x10) != 0x1 || (header & 0x07) != 0) {
Log.w(TAG, "RTP packet is not the start of a new VP8 partition, skipping.");
return false;
// TODO(b/198620566) Consider using ParsableBitArray.
// For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2.
if ((header & 0x10) == 0x10 && (header & 0x07) == 0) {
if (gotFirstPacketOfVp8Frame && fragmentedSampleSizeBytes > 0) {
// Received new VP8 fragment, output data of previous fragment to decoder.
outputSampleMetadataForFragmentedPackets();
}
gotFirstPacketOfVp8Frame = true;
} else {
} else if (gotFirstPacketOfVp8Frame) {
// Check that this packet is in the sequence of the previous packet.
int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber);
if (packetSequenceNumber != expectedSequenceNumber) {
if (packetSequenceNumber < expectedSequenceNumber) {
Log.w(
TAG,
Util.formatInvariant(
@ -167,6 +170,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
expectedSequenceNumber, packetSequenceNumber));
return false;
}
} else {
Log.w(TAG, "RTP packet is not the start of a new VP8 partition, skipping.");
return false;
}
// Check if optional X header is present.
@ -195,6 +201,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return true;
}
/**
* Outputs sample metadata of the received fragmented packets.
*
* <p>Call this method only after receiving an end of a VP8 partition.
*/
private void outputSampleMetadataForFragmentedPackets() {
checkNotNull(trackOutput)
.sampleMetadata(
fragmentedSampleTimeUs,
isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
fragmentedSampleSizeBytes,
/* offset= */ 0,
/* cryptoData= */ null);
fragmentedSampleSizeBytes = 0;
fragmentedSampleTimeUs = C.TIME_UNSET;
gotFirstPacketOfVp8Frame = false;
}
private static long toSampleUs(
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) {
return startTimeOffsetUs

View File

@ -0,0 +1,203 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.rtsp.reader;
import static androidx.media3.common.util.Util.getBytesFromHexString;
import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.rtsp.RtpPacket;
import androidx.media3.exoplayer.rtsp.RtpPayloadFormat;
import androidx.media3.test.utils.FakeExtractorOutput;
import androidx.media3.test.utils.FakeTrackOutput;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Bytes;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link RtpVp8Reader}. */
@RunWith(AndroidJUnit4.class)
public final class RtpVp8ReaderTest {
/** VP9 uses a 90 KHz media clock (RFC7741 Section 4.1). */
private static final long MEDIA_CLOCK_FREQUENCY = 90_000;
private static final byte[] PARTITION_1 = getBytesFromHexString("000102030405060708090A0B0C0D0E");
// 000102030405060708090A
private static final byte[] PARTITION_1_FRAGMENT_1 =
Arrays.copyOf(PARTITION_1, /* newLength= */ 11);
// 0B0C0D0E
private static final byte[] PARTITION_1_FRAGMENT_2 =
Arrays.copyOfRange(PARTITION_1, /* from= */ 11, /* to= */ 15);
private static final long PARTITION_1_RTP_TIMESTAMP = 2599168056L;
private static final RtpPacket PACKET_PARTITION_1_FRAGMENT_1 =
new RtpPacket.Builder()
.setTimestamp(PARTITION_1_RTP_TIMESTAMP)
.setSequenceNumber(40289)
.setMarker(false)
.setPayloadData(Bytes.concat(getBytesFromHexString("10"), PARTITION_1_FRAGMENT_1))
.build();
private static final RtpPacket PACKET_PARTITION_1_FRAGMENT_2 =
new RtpPacket.Builder()
.setTimestamp(PARTITION_1_RTP_TIMESTAMP)
.setSequenceNumber(40290)
.setMarker(false)
.setPayloadData(Bytes.concat(getBytesFromHexString("00"), PARTITION_1_FRAGMENT_2))
.build();
private static final byte[] PARTITION_2 = getBytesFromHexString("0D0C0B0A09080706050403020100");
// 0D0C0B0A090807060504
private static final byte[] PARTITION_2_FRAGMENT_1 =
Arrays.copyOf(PARTITION_2, /* newLength= */ 10);
// 03020100
private static final byte[] PARTITION_2_FRAGMENT_2 =
Arrays.copyOfRange(PARTITION_2, /* from= */ 10, /* to= */ 14);
private static final long PARTITION_2_RTP_TIMESTAMP = 2599168344L;
private static final RtpPacket PACKET_PARTITION_2_FRAGMENT_1 =
new RtpPacket.Builder()
.setTimestamp(PARTITION_2_RTP_TIMESTAMP)
.setSequenceNumber(40291)
.setMarker(false)
.setPayloadData(Bytes.concat(getBytesFromHexString("10"), PARTITION_2_FRAGMENT_1))
.build();
private static final RtpPacket PACKET_PARTITION_2_FRAGMENT_2 =
new RtpPacket.Builder()
.setTimestamp(PARTITION_2_RTP_TIMESTAMP)
.setSequenceNumber(40292)
.setMarker(true)
.setPayloadData(
Bytes.concat(
getBytesFromHexString("80"),
// Optional header.
getBytesFromHexString("D6AA953961"),
PARTITION_2_FRAGMENT_2))
.build();
private static final long PARTITION_2_PRESENTATION_TIMESTAMP_US =
Util.scaleLargeTimestamp(
(PARTITION_2_RTP_TIMESTAMP - PARTITION_1_RTP_TIMESTAMP),
/* multiplier= */ C.MICROS_PER_SECOND,
/* divisor= */ MEDIA_CLOCK_FREQUENCY);
private FakeExtractorOutput extractorOutput;
@Before
public void setUp() {
extractorOutput =
new FakeExtractorOutput(
(id, type) -> new FakeTrackOutput(/* deduplicateConsecutiveFormats= */ true));
}
@Test
public void consume_validPackets() {
RtpVp8Reader vp8Reader = createVp8Reader();
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
vp8Reader.onReceivingFirstPacket(
PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
}
@Test
public void consume_fragmentedFrameMissingFirstFragment() {
RtpVp8Reader vp8Reader = createVp8Reader();
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
// First packet timing information is transmitted over RTSP, not RTP.
vp8Reader.onReceivingFirstPacket(
PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertThat(trackOutput.getSampleCount()).isEqualTo(1);
assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_2);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
}
@Test
public void consume_fragmentedFrameMissingBoundaryFragment() {
RtpVp8Reader vp8Reader = createVp8Reader();
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
vp8Reader.onReceivingFirstPacket(
PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1_FRAGMENT_1);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
}
@Test
public void consume_outOfOrderFragmentedFrame() {
RtpVp8Reader vp8Reader = createVp8Reader();
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
vp8Reader.onReceivingFirstPacket(
PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1_FRAGMENT_1);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
}
private static RtpVp8Reader createVp8Reader() {
return new RtpVp8Reader(
new RtpPayloadFormat(
new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_VP8).build(),
/* rtpPayloadType= */ 96,
/* clockRate= */ (int) MEDIA_CLOCK_FREQUENCY,
/* fmtpParameters= */ ImmutableMap.of()));
}
private static void consume(RtpVp8Reader vp8Reader, RtpPacket rtpPacket) {
vp8Reader.consume(
new ParsableByteArray(rtpPacket.payloadData),
rtpPacket.timestamp,
rtpPacket.sequenceNumber,
rtpPacket.marker);
}
}

View File

@ -29,6 +29,7 @@ dependencies {
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
testImplementation project(modulePrefix + 'test-utils-robolectric')
testImplementation project(modulePrefix + 'test-utils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}

View File

@ -15,16 +15,21 @@
*/
package androidx.media3.exoplayer.smoothstreaming;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.test.utils.FakeDataSource;
import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -93,4 +98,53 @@ public class DefaultMediaSourceFactoryTest {
assertThat(supportedTypes).asList().containsExactly(C.CONTENT_TYPE_OTHER, C.CONTENT_TYPE_SS);
}
@Test
public void createMediaSource_withSetDataSourceFactory_usesDataSourceFactory() throws Exception {
FakeDataSource fakeDataSource = new FakeDataSource();
DefaultMediaSourceFactory defaultMediaSourceFactory =
new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext())
.setDataSourceFactory(() -> fakeDataSource);
prepareSsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
}
@Test
public void
createMediaSource_usingDefaultDataSourceFactoryAndSetDataSourceFactory_usesUpdatesDataSourceFactory()
throws Exception {
FakeDataSource fakeDataSource = new FakeDataSource();
DefaultMediaSourceFactory defaultMediaSourceFactory =
new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext());
// Use default DataSource.Factory first.
prepareSsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
defaultMediaSourceFactory.setDataSourceFactory(() -> fakeDataSource);
prepareSsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
}
private static void prepareSsUrlAndWaitForPrepareError(
DefaultMediaSourceFactory defaultMediaSourceFactory) throws Exception {
MediaSource mediaSource =
defaultMediaSourceFactory.createMediaSource(MediaItem.fromUri(URI_MEDIA + "/file.ism"));
getInstrumentation()
.runOnMainSync(
() ->
mediaSource.prepareSource(
(source, timeline) -> {}, /* mediaTransferListener= */ null, PlayerId.UNSET));
// We don't expect this to prepare successfully.
RobolectricUtil.runMainLooperUntil(
() -> {
try {
mediaSource.maybeThrowSourceInfoRefreshError();
return false;
} catch (IOException e) {
return true;
}
});
}
}

View File

@ -18,6 +18,7 @@ package androidx.media3.extractor;
import static java.lang.Math.min;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
@ -786,40 +787,105 @@ public final class NalUnitUtil {
}
}
/**
* Skips any short term reference picture sets contained in a SPS.
*
* <p>Note: The st_ref_pic_set parsing in this method is simplified for the case where they're
* contained in a SPS, and would need generalizing for use elsewhere.
*/
private static void skipShortTermReferencePictureSets(ParsableNalUnitBitArray bitArray) {
int numShortTermRefPicSets = bitArray.readUnsignedExpGolombCodedInt();
boolean interRefPicSetPredictionFlag = false;
int numNegativePics;
int numPositivePics;
// As this method applies in a SPS, the only element of NumDeltaPocs accessed is the previous
// one, so we just keep track of that rather than storing the whole array.
// RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1) and delta_idx_minus1 is always zero in SPS.
int previousNumDeltaPocs = 0;
// As this method applies in a SPS, each short term reference picture set only accesses data
// from the previous one. This is because RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1), and
// delta_idx_minus1 is always zero in a SPS. Hence we just keep track of variables from the
// previous one as we iterate.
int previousNumNegativePics = C.INDEX_UNSET;
int previousNumPositivePics = C.INDEX_UNSET;
int[] previousDeltaPocS0 = new int[0];
int[] previousDeltaPocS1 = new int[0];
for (int stRpsIdx = 0; stRpsIdx < numShortTermRefPicSets; stRpsIdx++) {
if (stRpsIdx != 0) {
interRefPicSetPredictionFlag = bitArray.readBit();
}
int numNegativePics;
int numPositivePics;
int[] deltaPocS0;
int[] deltaPocS1;
boolean interRefPicSetPredictionFlag = stRpsIdx != 0 && bitArray.readBit();
if (interRefPicSetPredictionFlag) {
bitArray.skipBit(); // delta_rps_sign
bitArray.readUnsignedExpGolombCodedInt(); // abs_delta_rps_minus1
int previousNumDeltaPocs = previousNumNegativePics + previousNumPositivePics;
int deltaRpsSign = bitArray.readBit() ? 1 : 0;
int absDeltaRps = bitArray.readUnsignedExpGolombCodedInt() + 1;
int deltaRps = (1 - 2 * deltaRpsSign) * absDeltaRps;
boolean[] useDeltaFlags = new boolean[previousNumDeltaPocs + 1];
for (int j = 0; j <= previousNumDeltaPocs; j++) {
if (!bitArray.readBit()) { // used_by_curr_pic_flag[j]
bitArray.skipBit(); // use_delta_flag[j]
useDeltaFlags[j] = bitArray.readBit();
} else {
// When use_delta_flag[j] is not present, its value is 1.
useDeltaFlags[j] = true;
}
}
// Derive numNegativePics, numPositivePics, deltaPocS0 and deltaPocS1 as per Rec. ITU-T
// H.265 v6 (06/2019) Section 7.4.8
int i = 0;
deltaPocS0 = new int[previousNumDeltaPocs + 1];
deltaPocS1 = new int[previousNumDeltaPocs + 1];
for (int j = previousNumPositivePics - 1; j >= 0; j--) {
int dPoc = previousDeltaPocS1[j] + deltaRps;
if (dPoc < 0 && useDeltaFlags[previousNumNegativePics + j]) {
deltaPocS0[i++] = dPoc;
}
}
if (deltaRps < 0 && useDeltaFlags[previousNumDeltaPocs]) {
deltaPocS0[i++] = deltaRps;
}
for (int j = 0; j < previousNumNegativePics; j++) {
int dPoc = previousDeltaPocS0[j] + deltaRps;
if (dPoc < 0 && useDeltaFlags[j]) {
deltaPocS0[i++] = dPoc;
}
}
numNegativePics = i;
deltaPocS0 = Arrays.copyOf(deltaPocS0, numNegativePics);
i = 0;
for (int j = previousNumNegativePics - 1; j >= 0; j--) {
int dPoc = previousDeltaPocS0[j] + deltaRps;
if (dPoc > 0 && useDeltaFlags[j]) {
deltaPocS1[i++] = dPoc;
}
}
if (deltaRps > 0 && useDeltaFlags[previousNumDeltaPocs]) {
deltaPocS1[i++] = deltaRps;
}
for (int j = 0; j < previousNumPositivePics; j++) {
int dPoc = previousDeltaPocS1[j] + deltaRps;
if (dPoc > 0 && useDeltaFlags[previousNumNegativePics + j]) {
deltaPocS1[i++] = dPoc;
}
}
numPositivePics = i;
deltaPocS1 = Arrays.copyOf(deltaPocS1, numPositivePics);
} else {
numNegativePics = bitArray.readUnsignedExpGolombCodedInt();
numPositivePics = bitArray.readUnsignedExpGolombCodedInt();
previousNumDeltaPocs = numNegativePics + numPositivePics;
deltaPocS0 = new int[numNegativePics];
for (int i = 0; i < numNegativePics; i++) {
bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s0_minus1[i]
deltaPocS0[i] = bitArray.readUnsignedExpGolombCodedInt() + 1;
bitArray.skipBit(); // used_by_curr_pic_s0_flag[i]
}
deltaPocS1 = new int[numPositivePics];
for (int i = 0; i < numPositivePics; i++) {
bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s1_minus1[i]
deltaPocS1[i] = bitArray.readUnsignedExpGolombCodedInt() + 1;
bitArray.skipBit(); // used_by_curr_pic_s1_flag[i]
}
}
previousNumNegativePics = numNegativePics;
previousNumPositivePics = numPositivePics;
previousDeltaPocS0 = deltaPocS0;
previousDeltaPocS1 = deltaPocS1;
}
}

View File

@ -45,6 +45,7 @@ import androidx.media3.extractor.OpusUtil;
import androidx.media3.extractor.metadata.mp4.SmtaMetadataEntry;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
@ -1303,7 +1304,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
if (esdsData != null) {
formatBuilder.setAverageBitrate(esdsData.bitrate).setPeakBitrate(esdsData.peakBitrate);
formatBuilder
.setAverageBitrate(Ints.saturatedCast(esdsData.bitrate))
.setPeakBitrate(Ints.saturatedCast(esdsData.peakBitrate));
}
out.format = formatBuilder.build();
@ -1609,7 +1612,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
.setLanguage(language);
if (esdsData != null) {
formatBuilder.setAverageBitrate(esdsData.bitrate).setPeakBitrate(esdsData.peakBitrate);
formatBuilder
.setAverageBitrate(Ints.saturatedCast(esdsData.bitrate))
.setPeakBitrate(Ints.saturatedCast(esdsData.peakBitrate));
}
out.format = formatBuilder.build();
@ -1659,7 +1664,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
parent.skipBytes(2);
}
if ((flags & 0x40 /* URL_Flag */) != 0) {
parent.skipBytes(parent.readUnsignedShort());
parent.skipBytes(parent.readUnsignedByte());
}
if ((flags & 0x20 /* OCRstreamFlag */) != 0) {
parent.skipBytes(2);
@ -1683,8 +1688,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
parent.skipBytes(4);
int peakBitrate = parent.readUnsignedIntToInt();
int bitrate = parent.readUnsignedIntToInt();
long peakBitrate = parent.readUnsignedInt();
long bitrate = parent.readUnsignedInt();
// Start of the DecoderSpecificInfo.
parent.skipBytes(1); // DecoderSpecificInfo tag
@ -1943,14 +1948,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private static final class EsdsData {
private final @NullableType String mimeType;
private final byte @NullableType [] initializationData;
private final int bitrate;
private final int peakBitrate;
private final long bitrate;
private final long peakBitrate;
public EsdsData(
@NullableType String mimeType,
byte @NullableType [] initializationData,
int bitrate,
int peakBitrate) {
long bitrate,
long peakBitrate) {
this.mimeType = mimeType;
this.initializationData = initializationData;
this.bitrate = bitrate;

View File

@ -170,6 +170,32 @@ public final class NalUnitUtilTest {
assertDiscardToSpsMatchesExpected("FF00000001660000000167FF", "0000000167FF");
}
/** Regression test for https://github.com/google/ExoPlayer/issues/10316. */
@Test
public void parseH265SpsNalUnitPayload_exoghi_10316() {
byte[] spsNalUnitPayload =
new byte[] {
1, 2, 32, 0, 0, 3, 0, -112, 0, 0, 3, 0, 0, 3, 0, -106, -96, 1, -32, 32, 2, 28, 77, -98,
87, -110, 66, -111, -123, 22, 74, -86, -53, -101, -98, -68, -28, 9, 119, -21, -103, 120,
-16, 22, -95, 34, 1, 54, -62, 0, 0, 7, -46, 0, 0, -69, -127, -12, 85, -17, 126, 0, -29,
-128, 28, 120, 1, -57, 0, 56, -15
};
NalUnitUtil.H265SpsData spsData =
NalUnitUtil.parseH265SpsNalUnitPayload(spsNalUnitPayload, 0, spsNalUnitPayload.length);
assertThat(spsData.constraintBytes).isEqualTo(new int[] {144, 0, 0, 0, 0, 0});
assertThat(spsData.generalLevelIdc).isEqualTo(150);
assertThat(spsData.generalProfileCompatibilityFlags).isEqualTo(4);
assertThat(spsData.generalProfileIdc).isEqualTo(2);
assertThat(spsData.generalProfileSpace).isEqualTo(0);
assertThat(spsData.generalTierFlag).isFalse();
assertThat(spsData.height).isEqualTo(2160);
assertThat(spsData.pixelWidthHeightRatio).isEqualTo(1);
assertThat(spsData.seqParameterSetId).isEqualTo(0);
assertThat(spsData.width).isEqualTo(3840);
}
private static byte[] buildTestData() {
byte[] data = new byte[20];
for (int i = 0; i < data.length; i++) {

View File

@ -122,6 +122,15 @@ public final class FragmentedMp4ExtractorTest {
simulationConfig);
}
/** https://github.com/google/ExoPlayer/issues/10381 */
@Test
public void sampleWithLargeBitrates() throws Exception {
ExtractorAsserts.assertBehavior(
getExtractorFactory(ImmutableList.of()),
"media/mp4/sample_fragmented_large_bitrates.mp4",
simulationConfig);
}
private static ExtractorFactory getExtractorFactory(final List<Format> closedCaptionFormats) {
return () ->
new FragmentedMp4Extractor(

View File

@ -69,6 +69,15 @@ import java.util.concurrent.ExecutionException;
* <li>{@link MediaController#COMMAND_SEEK_TO_NEXT} to seek to the next item.
* </ul>
*
* <h2>Custom commands</h2>
*
* Custom actions are sent to the session under the hood. You can receive them by overriding the
* session callback method {@link MediaSession.Callback#onCustomCommand(MediaSession,
* MediaSession.ControllerInfo, SessionCommand, Bundle)}. This is useful because starting with
* Android 13, the System UI notification sends commands directly to the session. So handling the
* custom commands on the session level allows you to handle them at the same callback for all API
* levels.
*
* <h2>Drawables</h2>
*
* The drawables used can be overridden by drawables with the same names defined the application.
@ -219,6 +228,14 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
* customized by defining the index of the command in compact view of up to 3 commands in their
* extras with key {@link DefaultMediaNotificationProvider#COMMAND_KEY_COMPACT_VIEW_INDEX}.
*
* <p>To make the custom layout and commands work, you need to {@linkplain
* MediaSession#setCustomLayout(List) set the custom layout of commands} and add the custom
* commands to the available commands when a controller {@linkplain
* MediaSession.Callback#onConnect(MediaSession, MediaSession.ControllerInfo) connects to the
* session}. Controllers that connect after you called {@link MediaSession#setCustomLayout(List)}
* need the custom command set in {@link MediaSession.Callback#onPostConnect(MediaSession,
* MediaSession.ControllerInfo)} also.
*
* @param playerCommands The available player commands.
* @param customLayout The {@linkplain MediaSession#setCustomLayout(List) custom layout of
* commands}.

View File

@ -0,0 +1,339 @@
seekMap:
isSeekable = true
duration = 1067733
getPosition(0) = [[timeUs=66733, position=1325]]
getPosition(1) = [[timeUs=66733, position=1325]]
getPosition(533866) = [[timeUs=66733, position=1325]]
getPosition(1067733) = [[timeUs=66733, position=1325]]
numberOfTracks = 2
track 0:
total output bytes = 85933
sample count = 30
format 0:
id = 1
sampleMimeType = video/avc
codecs = avc1.64001F
width = 1080
height = 720
initializationData:
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
time = 200200
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
time = 166833
flags = 0
data = length 564, hash E5F56C5B
sample 5:
time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
time = 300300
flags = 0
data = length 467, hash 69806D94
sample 9:
time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
time = 400400
flags = 0
data = length 1087, hash 491BF106
sample 11:
time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
time = 600600
flags = 0
data = length 5190, hash B9031D8
sample 14:
time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
time = 567233
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
time = 700700
flags = 0
data = length 491, hash B5930C7C
sample 21:
time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
time = 800800
flags = 0
data = length 838, hash 294A3451
sample 23:
time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
time = 1001000
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
time = 967633
flags = 0
data = length 415, hash 850DFEA3
sample 29:
time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
total output bytes = 18257
sample count = 46
format 0:
averageBitrate = 2147483647
peakBitrate = 2147483647
id = 2
sampleMimeType = audio/mp4a-latm
codecs = mp4a.40.2
channelCount = 1
sampleRate = 44100
language = und
initializationData:
data = length 5, hash 2B7623A
sample 0:
time = 0
flags = 1
data = length 18, hash 96519432
sample 1:
time = 23219
flags = 1
data = length 4, hash EE9DF
sample 2:
time = 46439
flags = 1
data = length 4, hash EEDBF
sample 3:
time = 69659
flags = 1
data = length 157, hash E2F078F4
sample 4:
time = 92879
flags = 1
data = length 371, hash B9471F94
sample 5:
time = 116099
flags = 1
data = length 373, hash 2AB265CB
sample 6:
time = 139319
flags = 1
data = length 402, hash 1295477C
sample 7:
time = 162539
flags = 1
data = length 455, hash 2D8146C8
sample 8:
time = 185759
flags = 1
data = length 434, hash F2C5D287
sample 9:
time = 208979
flags = 1
data = length 450, hash 84143FCD
sample 10:
time = 232199
flags = 1
data = length 429, hash EF769D50
sample 11:
time = 255419
flags = 1
data = length 450, hash EC3DE692
sample 12:
time = 278639
flags = 1
data = length 447, hash 3E519E13
sample 13:
time = 301859
flags = 1
data = length 457, hash 1E4F23A0
sample 14:
time = 325079
flags = 1
data = length 447, hash A439EA97
sample 15:
time = 348299
flags = 1
data = length 456, hash 1E9034C6
sample 16:
time = 371519
flags = 1
data = length 398, hash 99DB7345
sample 17:
time = 394739
flags = 1
data = length 474, hash 3F05F10A
sample 18:
time = 417959
flags = 1
data = length 416, hash C105EE09
sample 19:
time = 441179
flags = 1
data = length 454, hash 5FDBE458
sample 20:
time = 464399
flags = 1
data = length 438, hash 41A93AC3
sample 21:
time = 487619
flags = 1
data = length 443, hash 10FDA652
sample 22:
time = 510839
flags = 1
data = length 412, hash 1F791E25
sample 23:
time = 534058
flags = 1
data = length 482, hash A6D983D
sample 24:
time = 557278
flags = 1
data = length 386, hash BED7392F
sample 25:
time = 580498
flags = 1
data = length 463, hash 5309F8C9
sample 26:
time = 603718
flags = 1
data = length 394, hash 21C7321F
sample 27:
time = 626938
flags = 1
data = length 489, hash 71B4730D
sample 28:
time = 650158
flags = 1
data = length 403, hash D9C6DE89
sample 29:
time = 673378
flags = 1
data = length 447, hash 9B14B73B
sample 30:
time = 696598
flags = 1
data = length 439, hash 4760D35B
sample 31:
time = 719818
flags = 1
data = length 463, hash 1601F88D
sample 32:
time = 743038
flags = 1
data = length 423, hash D4AE6773
sample 33:
time = 766258
flags = 1
data = length 497, hash A3C674D3
sample 34:
time = 789478
flags = 1
data = length 419, hash D3734A1F
sample 35:
time = 812698
flags = 1
data = length 474, hash DFB41F9
sample 36:
time = 835918
flags = 1
data = length 413, hash 53E7CB9F
sample 37:
time = 859138
flags = 1
data = length 445, hash D15B0E39
sample 38:
time = 882358
flags = 1
data = length 453, hash 77ED81E4
sample 39:
time = 905578
flags = 1
data = length 545, hash 3321AEB9
sample 40:
time = 928798
flags = 1
data = length 317, hash F557D0E
sample 41:
time = 952018
flags = 1
data = length 537, hash ED58CF7B
sample 42:
time = 975238
flags = 1
data = length 458, hash 51CDAA10
sample 43:
time = 998458
flags = 1
data = length 465, hash CBA1EFD7
sample 44:
time = 1021678
flags = 1
data = length 446, hash D6735B8A
sample 45:
time = 1044897
flags = 1
data = length 10, hash A453EEBE
tracksEnded = true

View File

@ -0,0 +1,279 @@
seekMap:
isSeekable = true
duration = 1067733
getPosition(0) = [[timeUs=66733, position=1325]]
getPosition(1) = [[timeUs=66733, position=1325]]
getPosition(533866) = [[timeUs=66733, position=1325]]
getPosition(1067733) = [[timeUs=66733, position=1325]]
numberOfTracks = 2
track 0:
total output bytes = 85933
sample count = 30
format 0:
id = 1
sampleMimeType = video/avc
codecs = avc1.64001F
width = 1080
height = 720
initializationData:
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
time = 200200
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
time = 166833
flags = 0
data = length 564, hash E5F56C5B
sample 5:
time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
time = 300300
flags = 0
data = length 467, hash 69806D94
sample 9:
time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
time = 400400
flags = 0
data = length 1087, hash 491BF106
sample 11:
time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
time = 600600
flags = 0
data = length 5190, hash B9031D8
sample 14:
time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
time = 567233
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
time = 700700
flags = 0
data = length 491, hash B5930C7C
sample 21:
time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
time = 800800
flags = 0
data = length 838, hash 294A3451
sample 23:
time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
time = 1001000
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
time = 967633
flags = 0
data = length 415, hash 850DFEA3
sample 29:
time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
total output bytes = 13359
sample count = 31
format 0:
averageBitrate = 2147483647
peakBitrate = 2147483647
id = 2
sampleMimeType = audio/mp4a-latm
codecs = mp4a.40.2
channelCount = 1
sampleRate = 44100
language = und
initializationData:
data = length 5, hash 2B7623A
sample 0:
time = 348299
flags = 1
data = length 456, hash 1E9034C6
sample 1:
time = 371519
flags = 1
data = length 398, hash 99DB7345
sample 2:
time = 394739
flags = 1
data = length 474, hash 3F05F10A
sample 3:
time = 417959
flags = 1
data = length 416, hash C105EE09
sample 4:
time = 441179
flags = 1
data = length 454, hash 5FDBE458
sample 5:
time = 464399
flags = 1
data = length 438, hash 41A93AC3
sample 6:
time = 487619
flags = 1
data = length 443, hash 10FDA652
sample 7:
time = 510839
flags = 1
data = length 412, hash 1F791E25
sample 8:
time = 534058
flags = 1
data = length 482, hash A6D983D
sample 9:
time = 557278
flags = 1
data = length 386, hash BED7392F
sample 10:
time = 580498
flags = 1
data = length 463, hash 5309F8C9
sample 11:
time = 603718
flags = 1
data = length 394, hash 21C7321F
sample 12:
time = 626938
flags = 1
data = length 489, hash 71B4730D
sample 13:
time = 650158
flags = 1
data = length 403, hash D9C6DE89
sample 14:
time = 673378
flags = 1
data = length 447, hash 9B14B73B
sample 15:
time = 696598
flags = 1
data = length 439, hash 4760D35B
sample 16:
time = 719818
flags = 1
data = length 463, hash 1601F88D
sample 17:
time = 743038
flags = 1
data = length 423, hash D4AE6773
sample 18:
time = 766258
flags = 1
data = length 497, hash A3C674D3
sample 19:
time = 789478
flags = 1
data = length 419, hash D3734A1F
sample 20:
time = 812698
flags = 1
data = length 474, hash DFB41F9
sample 21:
time = 835918
flags = 1
data = length 413, hash 53E7CB9F
sample 22:
time = 859138
flags = 1
data = length 445, hash D15B0E39
sample 23:
time = 882358
flags = 1
data = length 453, hash 77ED81E4
sample 24:
time = 905578
flags = 1
data = length 545, hash 3321AEB9
sample 25:
time = 928798
flags = 1
data = length 317, hash F557D0E
sample 26:
time = 952018
flags = 1
data = length 537, hash ED58CF7B
sample 27:
time = 975238
flags = 1
data = length 458, hash 51CDAA10
sample 28:
time = 998458
flags = 1
data = length 465, hash CBA1EFD7
sample 29:
time = 1021678
flags = 1
data = length 446, hash D6735B8A
sample 30:
time = 1044897
flags = 1
data = length 10, hash A453EEBE
tracksEnded = true

View File

@ -0,0 +1,219 @@
seekMap:
isSeekable = true
duration = 1067733
getPosition(0) = [[timeUs=66733, position=1325]]
getPosition(1) = [[timeUs=66733, position=1325]]
getPosition(533866) = [[timeUs=66733, position=1325]]
getPosition(1067733) = [[timeUs=66733, position=1325]]
numberOfTracks = 2
track 0:
total output bytes = 85933
sample count = 30
format 0:
id = 1
sampleMimeType = video/avc
codecs = avc1.64001F
width = 1080
height = 720
initializationData:
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
time = 200200
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
time = 166833
flags = 0
data = length 564, hash E5F56C5B
sample 5:
time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
time = 300300
flags = 0
data = length 467, hash 69806D94
sample 9:
time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
time = 400400
flags = 0
data = length 1087, hash 491BF106
sample 11:
time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
time = 600600
flags = 0
data = length 5190, hash B9031D8
sample 14:
time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
time = 567233
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
time = 700700
flags = 0
data = length 491, hash B5930C7C
sample 21:
time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
time = 800800
flags = 0
data = length 838, hash 294A3451
sample 23:
time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
time = 1001000
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
time = 967633
flags = 0
data = length 415, hash 850DFEA3
sample 29:
time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
total output bytes = 6804
sample count = 16
format 0:
averageBitrate = 2147483647
peakBitrate = 2147483647
id = 2
sampleMimeType = audio/mp4a-latm
codecs = mp4a.40.2
channelCount = 1
sampleRate = 44100
language = und
initializationData:
data = length 5, hash 2B7623A
sample 0:
time = 696598
flags = 1
data = length 439, hash 4760D35B
sample 1:
time = 719818
flags = 1
data = length 463, hash 1601F88D
sample 2:
time = 743038
flags = 1
data = length 423, hash D4AE6773
sample 3:
time = 766258
flags = 1
data = length 497, hash A3C674D3
sample 4:
time = 789478
flags = 1
data = length 419, hash D3734A1F
sample 5:
time = 812698
flags = 1
data = length 474, hash DFB41F9
sample 6:
time = 835918
flags = 1
data = length 413, hash 53E7CB9F
sample 7:
time = 859138
flags = 1
data = length 445, hash D15B0E39
sample 8:
time = 882358
flags = 1
data = length 453, hash 77ED81E4
sample 9:
time = 905578
flags = 1
data = length 545, hash 3321AEB9
sample 10:
time = 928798
flags = 1
data = length 317, hash F557D0E
sample 11:
time = 952018
flags = 1
data = length 537, hash ED58CF7B
sample 12:
time = 975238
flags = 1
data = length 458, hash 51CDAA10
sample 13:
time = 998458
flags = 1
data = length 465, hash CBA1EFD7
sample 14:
time = 1021678
flags = 1
data = length 446, hash D6735B8A
sample 15:
time = 1044897
flags = 1
data = length 10, hash A453EEBE
tracksEnded = true

View File

@ -0,0 +1,159 @@
seekMap:
isSeekable = true
duration = 1067733
getPosition(0) = [[timeUs=66733, position=1325]]
getPosition(1) = [[timeUs=66733, position=1325]]
getPosition(533866) = [[timeUs=66733, position=1325]]
getPosition(1067733) = [[timeUs=66733, position=1325]]
numberOfTracks = 2
track 0:
total output bytes = 85933
sample count = 30
format 0:
id = 1
sampleMimeType = video/avc
codecs = avc1.64001F
width = 1080
height = 720
initializationData:
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
time = 200200
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
time = 166833
flags = 0
data = length 564, hash E5F56C5B
sample 5:
time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
time = 300300
flags = 0
data = length 467, hash 69806D94
sample 9:
time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
time = 400400
flags = 0
data = length 1087, hash 491BF106
sample 11:
time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
time = 600600
flags = 0
data = length 5190, hash B9031D8
sample 14:
time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
time = 567233
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
time = 700700
flags = 0
data = length 491, hash B5930C7C
sample 21:
time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
time = 800800
flags = 0
data = length 838, hash 294A3451
sample 23:
time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
time = 1001000
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
time = 967633
flags = 0
data = length 415, hash 850DFEA3
sample 29:
time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
total output bytes = 10
sample count = 1
format 0:
averageBitrate = 2147483647
peakBitrate = 2147483647
id = 2
sampleMimeType = audio/mp4a-latm
codecs = mp4a.40.2
channelCount = 1
sampleRate = 44100
language = und
initializationData:
data = length 5, hash 2B7623A
sample 0:
time = 1044897
flags = 1
data = length 10, hash A453EEBE
tracksEnded = true

View File

@ -0,0 +1,339 @@
seekMap:
isSeekable = true
duration = 1067733
getPosition(0) = [[timeUs=66733, position=1325]]
getPosition(1) = [[timeUs=66733, position=1325]]
getPosition(533866) = [[timeUs=66733, position=1325]]
getPosition(1067733) = [[timeUs=66733, position=1325]]
numberOfTracks = 2
track 0:
total output bytes = 85933
sample count = 30
format 0:
id = 1
sampleMimeType = video/avc
codecs = avc1.64001F
width = 1080
height = 720
initializationData:
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
time = 200200
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
time = 166833
flags = 0
data = length 564, hash E5F56C5B
sample 5:
time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
time = 300300
flags = 0
data = length 467, hash 69806D94
sample 9:
time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
time = 400400
flags = 0
data = length 1087, hash 491BF106
sample 11:
time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
time = 600600
flags = 0
data = length 5190, hash B9031D8
sample 14:
time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
time = 567233
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
time = 700700
flags = 0
data = length 491, hash B5930C7C
sample 21:
time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
time = 800800
flags = 0
data = length 838, hash 294A3451
sample 23:
time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
time = 1001000
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
time = 967633
flags = 0
data = length 415, hash 850DFEA3
sample 29:
time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
total output bytes = 18257
sample count = 46
format 0:
averageBitrate = 2147483647
peakBitrate = 2147483647
id = 2
sampleMimeType = audio/mp4a-latm
codecs = mp4a.40.2
channelCount = 1
sampleRate = 44100
language = und
initializationData:
data = length 5, hash 2B7623A
sample 0:
time = 0
flags = 1
data = length 18, hash 96519432
sample 1:
time = 23219
flags = 1
data = length 4, hash EE9DF
sample 2:
time = 46439
flags = 1
data = length 4, hash EEDBF
sample 3:
time = 69659
flags = 1
data = length 157, hash E2F078F4
sample 4:
time = 92879
flags = 1
data = length 371, hash B9471F94
sample 5:
time = 116099
flags = 1
data = length 373, hash 2AB265CB
sample 6:
time = 139319
flags = 1
data = length 402, hash 1295477C
sample 7:
time = 162539
flags = 1
data = length 455, hash 2D8146C8
sample 8:
time = 185759
flags = 1
data = length 434, hash F2C5D287
sample 9:
time = 208979
flags = 1
data = length 450, hash 84143FCD
sample 10:
time = 232199
flags = 1
data = length 429, hash EF769D50
sample 11:
time = 255419
flags = 1
data = length 450, hash EC3DE692
sample 12:
time = 278639
flags = 1
data = length 447, hash 3E519E13
sample 13:
time = 301859
flags = 1
data = length 457, hash 1E4F23A0
sample 14:
time = 325079
flags = 1
data = length 447, hash A439EA97
sample 15:
time = 348299
flags = 1
data = length 456, hash 1E9034C6
sample 16:
time = 371519
flags = 1
data = length 398, hash 99DB7345
sample 17:
time = 394739
flags = 1
data = length 474, hash 3F05F10A
sample 18:
time = 417959
flags = 1
data = length 416, hash C105EE09
sample 19:
time = 441179
flags = 1
data = length 454, hash 5FDBE458
sample 20:
time = 464399
flags = 1
data = length 438, hash 41A93AC3
sample 21:
time = 487619
flags = 1
data = length 443, hash 10FDA652
sample 22:
time = 510839
flags = 1
data = length 412, hash 1F791E25
sample 23:
time = 534058
flags = 1
data = length 482, hash A6D983D
sample 24:
time = 557278
flags = 1
data = length 386, hash BED7392F
sample 25:
time = 580498
flags = 1
data = length 463, hash 5309F8C9
sample 26:
time = 603718
flags = 1
data = length 394, hash 21C7321F
sample 27:
time = 626938
flags = 1
data = length 489, hash 71B4730D
sample 28:
time = 650158
flags = 1
data = length 403, hash D9C6DE89
sample 29:
time = 673378
flags = 1
data = length 447, hash 9B14B73B
sample 30:
time = 696598
flags = 1
data = length 439, hash 4760D35B
sample 31:
time = 719818
flags = 1
data = length 463, hash 1601F88D
sample 32:
time = 743038
flags = 1
data = length 423, hash D4AE6773
sample 33:
time = 766258
flags = 1
data = length 497, hash A3C674D3
sample 34:
time = 789478
flags = 1
data = length 419, hash D3734A1F
sample 35:
time = 812698
flags = 1
data = length 474, hash DFB41F9
sample 36:
time = 835918
flags = 1
data = length 413, hash 53E7CB9F
sample 37:
time = 859138
flags = 1
data = length 445, hash D15B0E39
sample 38:
time = 882358
flags = 1
data = length 453, hash 77ED81E4
sample 39:
time = 905578
flags = 1
data = length 545, hash 3321AEB9
sample 40:
time = 928798
flags = 1
data = length 317, hash F557D0E
sample 41:
time = 952018
flags = 1
data = length 537, hash ED58CF7B
sample 42:
time = 975238
flags = 1
data = length 458, hash 51CDAA10
sample 43:
time = 998458
flags = 1
data = length 465, hash CBA1EFD7
sample 44:
time = 1021678
flags = 1
data = length 446, hash D6735B8A
sample 45:
time = 1044897
flags = 1
data = length 10, hash A453EEBE
tracksEnded = true

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Includes ContentProtection elements with additional ClearKey license URLs.
Covers all possible locations (in AdaptationSet and Representation) and possible orders of these
ContentProtection elements (CENC first or ClearKey first).
-->
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:mpeg:DASH:schema:MPD:2011" xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd" minBufferTime="PT1.500S" profiles="urn:mpeg:dash:profile:isoff-main:2011" type="static" availabilityStartTime="2016-10-14T17:00:17" xmlns:cenc="urn:mpeg:cenc:2013" xmlns:clearkey="http://dashif.org/guidelines/clearKey">
<Period start="PT0.000S" duration="PT0H5M50S">
<SegmentTemplate startNumber="0" timescale="1000" media="sq/$Number$">
<SegmentTimeline>
<S d="2002" t="6009" r="2"/>
</SegmentTimeline>
</SegmentTemplate>
<AdaptationSet id="0" mimeType="audio/mp4" subsegmentAlignment="true">
<Representation id="140" codecs="mp4a.40.2" audioSamplingRate="48000" startWithSAP="1" bandwidth="144000">
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="9eb4050d-e44b-4802-932e-27d75083e266" />
<ContentProtection value="ClearKey1.0" schemeIdUri="urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e">
<clearkey:Laurl Lic_type="EME-1.0">https://testserver1.test/AcquireLicense</clearkey:Laurl>
</ContentProtection>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" mimeType="video/mp4" subsegmentAlignment="true">
<ContentProtection value="ClearKey1.0" schemeIdUri="urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e">
<clearkey:Laurl Lic_type="EME-1.0">https://testserver2.test/AcquireLicense</clearkey:Laurl>
</ContentProtection>
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="9eb4050d-e44b-4802-932e-27d75083e266" />
<Representation id="133" codecs="avc1.4d4015" width="426" height="240" startWithSAP="1" bandwidth="258000" frameRate="30" />
</AdaptationSet>
</Period>
</MPD>

View File

@ -0,0 +1,82 @@
MediaCodecAdapter (exotest.audio.aac):
buffers.length = 47
buffers[0] = length 18, hash 96519432
buffers[1] = length 4, hash EE9DF
buffers[2] = length 4, hash EEDBF
buffers[3] = length 157, hash E2F078F4
buffers[4] = length 371, hash B9471F94
buffers[5] = length 373, hash 2AB265CB
buffers[6] = length 402, hash 1295477C
buffers[7] = length 455, hash 2D8146C8
buffers[8] = length 434, hash F2C5D287
buffers[9] = length 450, hash 84143FCD
buffers[10] = length 429, hash EF769D50
buffers[11] = length 450, hash EC3DE692
buffers[12] = length 447, hash 3E519E13
buffers[13] = length 457, hash 1E4F23A0
buffers[14] = length 447, hash A439EA97
buffers[15] = length 456, hash 1E9034C6
buffers[16] = length 398, hash 99DB7345
buffers[17] = length 474, hash 3F05F10A
buffers[18] = length 416, hash C105EE09
buffers[19] = length 454, hash 5FDBE458
buffers[20] = length 438, hash 41A93AC3
buffers[21] = length 443, hash 10FDA652
buffers[22] = length 412, hash 1F791E25
buffers[23] = length 482, hash A6D983D
buffers[24] = length 386, hash BED7392F
buffers[25] = length 463, hash 5309F8C9
buffers[26] = length 394, hash 21C7321F
buffers[27] = length 489, hash 71B4730D
buffers[28] = length 403, hash D9C6DE89
buffers[29] = length 447, hash 9B14B73B
buffers[30] = length 439, hash 4760D35B
buffers[31] = length 463, hash 1601F88D
buffers[32] = length 423, hash D4AE6773
buffers[33] = length 497, hash A3C674D3
buffers[34] = length 419, hash D3734A1F
buffers[35] = length 474, hash DFB41F9
buffers[36] = length 413, hash 53E7CB9F
buffers[37] = length 445, hash D15B0E39
buffers[38] = length 453, hash 77ED81E4
buffers[39] = length 545, hash 3321AEB9
buffers[40] = length 317, hash F557D0E
buffers[41] = length 537, hash ED58CF7B
buffers[42] = length 458, hash 51CDAA10
buffers[43] = length 465, hash CBA1EFD7
buffers[44] = length 446, hash D6735B8A
buffers[45] = length 10, hash A453EEBE
buffers[46] = length 0, hash 1
MediaCodecAdapter (exotest.video.avc):
buffers.length = 31
buffers[0] = length 38070, hash B58E1AEE
buffers[1] = length 8340, hash 8AC449FF
buffers[2] = length 1295, hash C0DA5090
buffers[3] = length 469, hash D6E0A200
buffers[4] = length 564, hash E5F56C5B
buffers[5] = length 6075, hash 8756E49E
buffers[6] = length 847, hash DCC2B618
buffers[7] = length 455, hash B9CCE047
buffers[8] = length 467, hash 69806D94
buffers[9] = length 4549, hash 3944F501
buffers[10] = length 1087, hash 491BF106
buffers[11] = length 380, hash 5FED016A
buffers[12] = length 455, hash 8A0610
buffers[13] = length 5190, hash B9031D8
buffers[14] = length 1071, hash 684E7DC8
buffers[15] = length 653, hash 8494F326
buffers[16] = length 485, hash 2CCC85F4
buffers[17] = length 4884, hash D16B6A96
buffers[18] = length 997, hash 164FF210
buffers[19] = length 640, hash F664125B
buffers[20] = length 491, hash B5930C7C
buffers[21] = length 2989, hash 92CF4FCF
buffers[22] = length 838, hash 294A3451
buffers[23] = length 544, hash FCCE2DE6
buffers[24] = length 329, hash A654FFA1
buffers[25] = length 1517, hash 5F7EBF8B
buffers[26] = length 803, hash 7A5C4C1D
buffers[27] = length 415, hash B31BBC3B
buffers[28] = length 415, hash 850DFEA3
buffers[29] = length 619, hash AB5E56CA
buffers[30] = length 0, hash 1

View File

@ -15,6 +15,8 @@
*/
package androidx.media3.test.utils;
import static androidx.media3.test.utils.TestUtil.timelinesAreSame;
import android.os.Looper;
import android.view.Surface;
import androidx.annotation.Nullable;
@ -765,7 +767,7 @@ public abstract class Action {
@Nullable Timeline expectedTimeline,
@Player.TimelineChangeReason int expectedReason) {
super(tag, "WaitForTimelineChanged");
this.expectedTimeline = expectedTimeline != null ? new NoUidTimeline(expectedTimeline) : null;
this.expectedTimeline = expectedTimeline;
this.ignoreExpectedReason = false;
this.expectedReason = expectedReason;
}
@ -797,7 +799,7 @@ public abstract class Action {
@Override
public void onTimelineChanged(
Timeline timeline, @Player.TimelineChangeReason int reason) {
if ((expectedTimeline == null || new NoUidTimeline(timeline).equals(expectedTimeline))
if ((expectedTimeline == null || timelinesAreSame(timeline, expectedTimeline))
&& (ignoreExpectedReason || expectedReason == reason)) {
player.removeListener(this);
nextAction.schedule(player, trackSelector, surface, handler);
@ -805,8 +807,8 @@ public abstract class Action {
}
};
player.addListener(listener);
Timeline currentTimeline = new NoUidTimeline(player.getCurrentTimeline());
if (currentTimeline.equals(expectedTimeline)) {
if (expectedTimeline != null
&& timelinesAreSame(player.getCurrentTimeline(), expectedTimeline)) {
player.removeListener(listener);
nextAction.schedule(player, trackSelector, surface, handler);
}

View File

@ -43,6 +43,7 @@ import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -536,11 +537,8 @@ public final class ExoPlayerTestRunner implements Player.Listener, ActionSchedul
* @param timelines A list of expected {@link Timeline}s.
*/
public void assertTimelinesSame(Timeline... timelines) {
assertThat(this.timelines).hasSize(timelines.length);
for (int i = 0; i < timelines.length; i++) {
assertThat(new NoUidTimeline(timelines[i]))
.isEqualTo(new NoUidTimeline(this.timelines.get(i)));
}
TestUtil.assertTimelinesSame(
ImmutableList.copyOf(this.timelines), ImmutableList.copyOf(timelines));
}
/**

View File

@ -29,6 +29,7 @@ import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.source.ShuffleOrder;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
@ -275,7 +276,7 @@ public final class FakeTimeline extends Timeline {
private final TimelineWindowDefinition[] windowDefinitions;
private final Object[] manifests;
private final int[] periodOffsets;
private final FakeShuffleOrder fakeShuffleOrder;
private final ShuffleOrder shuffleOrder;
/**
* Returns an ad playback state with the specified number of ads in each of the specified ad
@ -395,6 +396,19 @@ public final class FakeTimeline extends Timeline {
* @param windowDefinitions A list of {@link TimelineWindowDefinition}s.
*/
public FakeTimeline(Object[] manifests, TimelineWindowDefinition... windowDefinitions) {
this(manifests, new FakeShuffleOrder(windowDefinitions.length), windowDefinitions);
}
/**
* Creates a fake timeline with the given window definitions and {@link
* androidx.media3.exoplayer.source.ShuffleOrder}.
*
* @param windowDefinitions A list of {@link TimelineWindowDefinition}s.
*/
public FakeTimeline(
Object[] manifests,
ShuffleOrder shuffleOrder,
TimelineWindowDefinition... windowDefinitions) {
this.manifests = new Object[windowDefinitions.length];
System.arraycopy(manifests, 0, this.manifests, 0, min(this.manifests.length, manifests.length));
this.windowDefinitions = windowDefinitions;
@ -403,7 +417,7 @@ public final class FakeTimeline extends Timeline {
for (int i = 0; i < windowDefinitions.length; i++) {
periodOffsets[i + 1] = periodOffsets[i] + windowDefinitions[i].periodCount;
}
fakeShuffleOrder = new FakeShuffleOrder(windowDefinitions.length);
this.shuffleOrder = shuffleOrder;
}
@Override
@ -422,7 +436,7 @@ public final class FakeTimeline extends Timeline {
? getFirstWindowIndex(shuffleModeEnabled)
: C.INDEX_UNSET;
}
return shuffleModeEnabled ? fakeShuffleOrder.getNextIndex(windowIndex) : windowIndex + 1;
return shuffleModeEnabled ? shuffleOrder.getNextIndex(windowIndex) : windowIndex + 1;
}
@Override
@ -436,20 +450,20 @@ public final class FakeTimeline extends Timeline {
? getLastWindowIndex(shuffleModeEnabled)
: C.INDEX_UNSET;
}
return shuffleModeEnabled ? fakeShuffleOrder.getPreviousIndex(windowIndex) : windowIndex - 1;
return shuffleModeEnabled ? shuffleOrder.getPreviousIndex(windowIndex) : windowIndex - 1;
}
@Override
public int getLastWindowIndex(boolean shuffleModeEnabled) {
return shuffleModeEnabled
? fakeShuffleOrder.getLastIndex()
? shuffleOrder.getLastIndex()
: super.getLastWindowIndex(/* shuffleModeEnabled= */ false);
}
@Override
public int getFirstWindowIndex(boolean shuffleModeEnabled) {
return shuffleModeEnabled
? fakeShuffleOrder.getFirstIndex()
? shuffleOrder.getFirstIndex()
: super.getFirstWindowIndex(/* shuffleModeEnabled= */ false);
}

View File

@ -1,51 +0,0 @@
/*
* Copyright (C) 2019 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 androidx.media3.test.utils;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.source.ForwardingTimeline;
/**
* A timeline which wraps another timeline and overrides all window and period uids to 0. This is
* useful for testing timeline equality without taking uids into account.
*/
@UnstableApi
public class NoUidTimeline extends ForwardingTimeline {
/**
* Creates an instance.
*
* @param timeline The underlying timeline.
*/
public NoUidTimeline(Timeline timeline) {
super(timeline);
}
@Override
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
timeline.getWindow(windowIndex, window, defaultPositionProjectionUs);
window.uid = 0;
return window;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
timeline.getPeriod(periodIndex, period, setIds);
period.uid = 0;
return period;
}
}

View File

@ -70,6 +70,7 @@ public class StubPlayer extends BasePlayer {
}
@Override
@Nullable
public PlaybackException getPlayerError() {
throw new UnsupportedOperationException();
}

View File

@ -44,6 +44,7 @@ import androidx.media3.extractor.SeekMap;
import androidx.media3.extractor.metadata.MetadataInputBuffer;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Bytes;
import com.google.common.truth.Correspondence;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@ -199,19 +200,33 @@ public class TestUtil {
/**
* Asserts that the actual timelines are the same to the expected timelines. This assert differs
* from testing equality by not comparing period ids which may be different due to id mapping of
* child source period ids.
* from testing equality by not comparing:
*
* <ul>
* <li>Period IDs, which may be different due to ID mapping of child source period IDs.
* <li>Shuffle order, which by default is random and non-deterministic.
* </ul>
*
* @param actualTimelines A list of actual {@link Timeline timelines}.
* @param expectedTimelines A list of expected {@link Timeline timelines}.
*/
public static void assertTimelinesSame(
List<Timeline> actualTimelines, List<Timeline> expectedTimelines) {
assertThat(actualTimelines).hasSize(expectedTimelines.size());
for (int i = 0; i < actualTimelines.size(); i++) {
assertThat(new NoUidTimeline(actualTimelines.get(i)))
.isEqualTo(new NoUidTimeline(expectedTimelines.get(i)));
}
assertThat(actualTimelines)
.comparingElementsUsing(
Correspondence.from(
TestUtil::timelinesAreSame, "is equal to (ignoring Window.uid and Period.uid)"))
.containsExactlyElementsIn(expectedTimelines)
.inOrder();
}
/**
* Returns true if {@code thisTimeline} is equal to {@code thatTimeline}, ignoring {@link
* Timeline.Window#uid} and {@link Timeline.Period#uid} values, and shuffle order.
*/
public static boolean timelinesAreSame(Timeline thisTimeline, Timeline thatTimeline) {
return new NoUidOrShufflingTimeline(thisTimeline)
.equals(new NoUidOrShufflingTimeline(thatTimeline));
}
/**
@ -494,4 +509,68 @@ public class TestUtil {
return list;
}
private static final class NoUidOrShufflingTimeline extends Timeline {
private final Timeline delegate;
public NoUidOrShufflingTimeline(Timeline timeline) {
this.delegate = timeline;
}
@Override
public int getWindowCount() {
return delegate.getWindowCount();
}
@Override
public int getNextWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) {
return delegate.getNextWindowIndex(windowIndex, repeatMode, /* shuffleModeEnabled= */ false);
}
@Override
public int getPreviousWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) {
return delegate.getPreviousWindowIndex(
windowIndex, repeatMode, /* shuffleModeEnabled= */ false);
}
@Override
public int getLastWindowIndex(boolean shuffleModeEnabled) {
return delegate.getLastWindowIndex(/* shuffleModeEnabled= */ false);
}
@Override
public int getFirstWindowIndex(boolean shuffleModeEnabled) {
return delegate.getFirstWindowIndex(/* shuffleModeEnabled= */ false);
}
@Override
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
delegate.getWindow(windowIndex, window, defaultPositionProjectionUs);
window.uid = 0;
return window;
}
@Override
public int getPeriodCount() {
return delegate.getPeriodCount();
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
delegate.getPeriod(periodIndex, period, setIds);
period.uid = 0;
return period;
}
@Override
public int getIndexOfPeriod(Object uid) {
return delegate.getIndexOfPeriod(uid);
}
@Override
public Object getUidOfPeriod(int periodIndex) {
return 0;
}
}
}

View File

@ -184,41 +184,6 @@ public class TestPlayerRunHelper {
return checkNotNull(player.getPlayerError());
}
/**
* Runs tasks of the main {@link Looper} until {@link
* ExoPlayer.AudioOffloadListener#onExperimentalOffloadSchedulingEnabledChanged} is called or a
* playback error occurs.
*
* <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
*
* @param player The {@link Player}.
* @return The new offloadSchedulingEnabled state.
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
* exceeded.
*/
public static boolean runUntilReceiveOffloadSchedulingEnabledNewState(ExoPlayer player)
throws TimeoutException {
verifyMainTestThread(player);
AtomicReference<@NullableType Boolean> offloadSchedulingEnabledReceiver =
new AtomicReference<>();
ExoPlayer.AudioOffloadListener listener =
new ExoPlayer.AudioOffloadListener() {
@Override
public void onExperimentalOffloadSchedulingEnabledChanged(
boolean offloadSchedulingEnabled) {
offloadSchedulingEnabledReceiver.set(offloadSchedulingEnabled);
}
};
player.addAudioOffloadListener(listener);
runMainLooperUntil(
() -> offloadSchedulingEnabledReceiver.get() != null || player.getPlayerError() != null);
player.removeAudioOffloadListener(listener);
if (player.getPlayerError() != null) {
throw new IllegalStateException(player.getPlayerError());
}
return checkNotNull(offloadSchedulingEnabledReceiver.get());
}
/**
* Runs tasks of the main {@link Looper} until {@link
* ExoPlayer.AudioOffloadListener#onExperimentalSleepingForOffloadChanged(boolean)} is called or a

View File

@ -1811,7 +1811,13 @@ public class PlayerControlView extends FrameLayout {
if (position < playbackSpeedTexts.length) {
holder.textView.setText(playbackSpeedTexts[position]);
}
holder.checkView.setVisibility(position == selectedIndex ? VISIBLE : INVISIBLE);
if (position == selectedIndex) {
holder.itemView.setSelected(true);
holder.checkView.setVisibility(VISIBLE);
} else {
holder.itemView.setSelected(false);
holder.checkView.setVisibility(INVISIBLE);
}
holder.itemView.setOnClickListener(
v -> {
if (position != selectedIndex) {

View File

@ -236,11 +236,6 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
// Player.Listener implementation.
@Override
public void onPlaybackStateChanged(@Player.State int playbackState) {
notifyStateChanged();
}
@Override
public void onPlayerError(PlaybackException error) {
Callback callback = getCallback();
@ -285,5 +280,13 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
int scaledWidth = Math.round(videoSize.width * videoSize.pixelWidthHeightRatio);
getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, scaledWidth, videoSize.height);
}
@Override
public void onEvents(Player player, Player.Events events) {
if (events.containsAny(
Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED)) {
notifyStateChanged();
}
}
}
}