mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
commit
ca6835bf0a
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
2
api.txt
2
api.txt
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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"/>
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
386
github/media3-migration.sh
Normal 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')"
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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++;
|
||||
|
@ -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();
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,16 +56,90 @@ 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() {
|
||||
synchronized (lock) {
|
||||
return logLevel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link LogLevel} for ExoPlayer logcat logging.
|
||||
@ -69,8 +147,10 @@ public final class Log {
|
||||
* @param logLevel The new {@link LogLevel}.
|
||||
*/
|
||||
public static void setLogLevel(@LogLevel int logLevel) {
|
||||
synchronized (lock) {
|
||||
Log.logLevel = logLevel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether stack traces of {@link Throwable}s will be logged to logcat. Stack trace logging
|
||||
@ -79,16 +159,31 @@ public final class Log {
|
||||
* @param logStackTraces Whether stack traces will be logged.
|
||||
*/
|
||||
public static void setLogStackTraces(boolean 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see android.util.Log#d(String, String)
|
||||
*/
|
||||
@Pure
|
||||
public static void d(@Size(max = 23) String tag, String message) {
|
||||
synchronized (lock) {
|
||||
if (logLevel == LOG_LEVEL_ALL) {
|
||||
android.util.Log.d(tag, message);
|
||||
logger.d(tag, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,8 +200,10 @@ public final class Log {
|
||||
*/
|
||||
@Pure
|
||||
public static void i(@Size(max = 23) String tag, String message) {
|
||||
synchronized (lock) {
|
||||
if (logLevel <= LOG_LEVEL_INFO) {
|
||||
android.util.Log.i(tag, message);
|
||||
logger.i(tag, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,8 +220,10 @@ public final class Log {
|
||||
*/
|
||||
@Pure
|
||||
public static void w(@Size(max = 23) String tag, String message) {
|
||||
synchronized (lock) {
|
||||
if (logLevel <= LOG_LEVEL_WARNING) {
|
||||
android.util.Log.w(tag, message);
|
||||
logger.w(tag, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,8 +240,10 @@ public final class Log {
|
||||
*/
|
||||
@Pure
|
||||
public static void e(@Size(max = 23) String tag, String message) {
|
||||
synchronized (lock) {
|
||||
if (logLevel <= LOG_LEVEL_ERROR) {
|
||||
android.util.Log.e(tag, message);
|
||||
logger.e(tag, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,11 +269,13 @@ public final class Log {
|
||||
@Nullable
|
||||
@Pure
|
||||
public static String getThrowableString(@Nullable Throwable throwable) {
|
||||
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
|
||||
// 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
|
||||
@ -184,6 +287,7 @@ public final class Log {
|
||||
return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Pure
|
||||
private static String appendThrowableString(String message, @Nullable Throwable throwable) {
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
for (int i = 0; i < sampleQueues.length; i++) {
|
||||
if (includeDisabledTracks || checkNotNull(trackState).trackEnabledStates[i]) {
|
||||
largestQueuedTimestampUs =
|
||||
max(largestQueuedTimestampUs, sampleQueue.getLargestQueuedTimestampUs());
|
||||
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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -1112,7 +1112,6 @@ public final class MediaPeriodQueueTest {
|
||||
/* bufferedPositionUs= */ 0,
|
||||
/* totalBufferedDurationUs= */ 0,
|
||||
/* positionUs= */ 0,
|
||||
/* offloadSchedulingEnabled= */ false,
|
||||
/* sleepingForOffload= */ false);
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
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;
|
||||
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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
// 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++) {
|
||||
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;
|
||||
for (int stRpsIdx = 0; stRpsIdx < numShortTermRefPicSets; stRpsIdx++) {
|
||||
if (stRpsIdx != 0) {
|
||||
interRefPicSetPredictionFlag = bitArray.readBit();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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++) {
|
||||
|
@ -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(
|
||||
|
@ -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}.
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
Binary file not shown.
@ -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>
|
@ -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
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -70,6 +70,7 @@ public class StubPlayer extends BasePlayer {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public PlaybackException getPlayerError() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user